القائمة الرئيسية

الصفحات

الوراثةوالتغليف في السي بلاس بلاس

C++ الوراثة

مفهوم الوراثة في C++

في البداية, كلمة وراثة تعني تضمين محتوى كلاس في كلاس آخر.
في C++ , الكلاس يمكنه أن يرث من كلاس آخر حتى يحصل على الدوال و المتغيرات الموجودة فيه.

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


مصطلحات تقنية

  • الوراثة تسمى Inheritance في اللغة الإنجليزية.

  • الكلاس الذي يرث من كلاس آخر يقال له الكلاس الإبن, و يسمى Subclass و يقال له أيضاً ( Derived Class, Extended Class أو Child Class ).

  • الكلاس الذي يورّث محتوياته لكلاس آخر يسمى الكلاس الأب, و يسمى Superclass و يقال له أيضاً ( Base Class أو Parent Class ).

أشكال الوراثة في C++

في C++ يوجد 4 أشكال للوراثة كما في الجدول التالي.


إذاَ أشكال الوراثة في C++ هي كالتالي:

  • وراثة فردية: تعني كلاس يرث من كلاس واحد فقط.

  • وراثة متتالية: تعني كلاس يرث من كلاس واحد و هذا الكلاس كان في الأصل يرث من كلاس آخر.

  • وراثة هرمية: تعني أن الكلاس موروث من قبل أكثر من كلاس.

  • وراثة متعددة: تعني أن الكلاس يرث من أكثر من كلاس.

كيفية جعل الكلاس يرث من كلاس آخر في C++

لجعل الكلاس يرث من كلاس آخر نتبع الأسلوب التالي.

class derived-class : access-specifier base-class
{

}

  • derived-class:   يقصد بها إسم الكلاس الإبن.

  • base-class:   يقصد بها إسم الكلاس الأب.

  • access-specifier:   يقصد بها تحديد كيف يمكن الوصول للأشياء التي تم تضمينها في الكلاس الأب في الكلاس الإبن.


إذاَ بالمبدأ لجعل الكلاس يرث من كلاس آخر نضع بعد إسم الكلاس نقطتين فوق بعض, ثم إسم الكلاس الذي نريده أن يرث منه.
مكان الكلمة access-specifier يمكنك وضع public أو private أو protected أو عدم وضع أي كلمة منهم و عندها سيتم إعتبار أنك وضعت الكلمة private.

كلمات الوصول في C++

عندما تجعل الكلاس يرث من كلاس آخر يمكنك استخدام إحدى كلمات الوصول (Access Specifier ) لتحدد كيف يمكن الوصول للأشياء التي ورثها الكلاس الإبن من الكلاس الأب.


الكلمة public

تستخدم لتحديد أن الأشياء التي سيرثها الكلاس الإبن من الكلاس الأب سيكون الوصول لها ممكناً من أي مكان.

مثال: class B : public A
هنا أي شيء يرثه الكلاس B من الكلاس A سيتم إعتبار كأنه تم تعريفه public في الكلاس B.



الكلمة private

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

مثال: class B : private A
هنا أي شيء يرثه الكلاس B من الكلاس A سيتم إعتبار كأنه تم تعريفه private في الكلاس B.



الكلمة protected

تستخدم لتحديد أن الأشياء التي سيرثها الكلاس الإبن من الكلاس الأب يمكن الوصول لها من نفس اكلاس و من أي كلاس آخر يرثها.

مثال: class B : protected A
هنا أي شيء يرثه الكلاس B من الكلاس A سيتم إعتبار كأنه تم تعريفه protected في الكلاس B.

أمثلة تطبيقية على الوراثة في C++


مثال عن الوراثة الفردية

في المثال التالي قمنا بتعريف كلاس إسمه A يحتوي على متغير إسمه x و دالة إسمها printMessage().
بعدها قمنا بإنشاء كلاس إسمه B يحتوي على متغير إسمه y و يرث من الكلاس A.

شكل الوراثة سيكون كالتالي.

المثال الأول

main.cpp
		#include <iostream>

		using namespace std;

		// printMessage و دالة إسمها x يحتوي على متغير إسمه A هنا قمنا بتعريف كلاس إسمه
		class A {

		public: 
		int x = 10;

		void printMessage()
		{
		cout << "Hello from class A \n";
		}

		};

		// y و يحتوي على متغير إسمه A يرث من الكلاس B هنا قمنا بتعريف كلاس إسمه
		class B: public A {

		public:
		int y = 20;

		};

		// main() هنا قمنا بتعريف الدالة
		int main()
		{
		// b إسمه B هنا قمنا بإنشاء كائن من الكلاس
		B b;

		// B و الذي تم تعريفه في الكلاس b الموجود في الكائن y هنا قمنا بطباعة قيمة المتغير
		cout << "y = " << b.y << "\n";

		// A من الكلاس B  و الذي ورثه الكلاس b الموجود في الكائن x هنا قمنا بطباعة قيمة المتغير
		cout << "x = " << b.x << "\n";

		// A من الكلاس B و التي ورثها الكلاس b الموجودة في الكائن printMessage() هنا قمنا باستدعاء الدالة
		b.printMessage();

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		y = 20
		x = 10
		Hello from class A
	  


مثال عن الوراثة المتتالية

في المثال التالي قمنا بتعريف كلاس إسمه A يحتوي على دالة إسمها printA().
بعدها قمنا بإنشاء كلاس إسمه B يحتوي على دالة إسمها printB() و يرث من الكلاس A.
بعدها قمنا بإنشاء كلاس إسمه C يحتوي على دالة إسمها printC() و يرث من الكلاس B.

شكل الوراثة سيكون كالتالي.

المثال الثاني

main.cpp
		#include <iostream>

		using namespace std;

		// printA يحتوي على دالة إسمها A هنا قمنا بتعريف كلاس إسمه
		class A {

		public: 
		void printA()
		{
		cout << "Hello from class A \n";
		}

		};

		// printB و يحتوي على دالة إسمها A يرث من الكلاس B هنا قمنا بتعريف كلاس إسمه
		class B: public A {

		public:
		void printB()
		{
		cout << "Hello from class B \n";
		}

		};

		// printC و يحتوي على دالة إسمها B يرث من الكلاس C هنا قمنا بتعريف كلاس إسمه
		class C: public B {

		public:
		void printC()
		{
		cout << "Hello from class C \n";
		}

		};

		// main() هنا قمنا بتعريف الدالة
		int main()
		{
		// c إسمه C هنا قمنا بإنشاء كائن من الكلاس
		C c;

		// c هنا قمنا باستدعاء جميع الدوال الموجودة في الكائن
		c.printA();
		c.printB();
		c.printC();

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		Hello from class A
		Hello from class B
		Hello from class C
	  


مثال عن الوراثة المتعددة

في المثال التالي قمنا بتعريف كلاس إسمه A يحتوي على دالة إسمها printA().
بعدها قمنا بإنشاء كلاس إسمه B يحتوي على دالة إسمها printB().
بعدها قمنا بإنشاء كلاس إسمه C يحتوي على دالة إسمها printC() و يرث من الكلاس A و الكلاس B.

شكل الوراثة سيكون كالتالي.

المثال الثالث

main.cpp
		#include <iostream>

		using namespace std;

		// printA يحتوي على دالة إسمها A هنا قمنا بتعريف كلاس إسمه
		class A {

		public: 
		void printA()
		{
		cout << "Hello from class A \n";
		}

		};

		// printB يحتوي على دالة إسمها B هنا قمنا بتعريف كلاس إسمه
		class B {

		public:
		void printB()
		{
		cout << "Hello from class B \n";
		}

		};

		// printC و يحتوي على دالة إسمها B و يرث من الكلاس A يرث من الكلاس C هنا قمنا بتعريف كلاس إسمه
		class C: public A, public B {

		public:
		void printC()
		{
		cout << "Hello from class C \n";
		}

		};

		// main() هنا قمنا بتعريف الدالة
		int main()
		{
		// c إسمه C هنا قمنا بإنشاء كائن من الكلاس
		C c;

		// c هنا قمنا باستدعاء جميع الدوال الموجودة في الكائن
		c.printA();
		c.printB();
		c.printC();

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		Hello from class A
		Hello from class B
		Hello from class C
	  


مثال عن الوراثة الهرمية

في المثال التالي قمنا بتعريف كلاس إسمه A يحتوي على دالة إسمها printA().
بعدها قمنا بإنشاء كلاس إسمه B يحتوي على دالة إسمها printB() و يرث من الكلاس A.
بعدها قمنا بإنشاء كلاس إسمه C يحتوي على دالة إسمها printC() و يرث من الكلاس A أيضاً.

شكل الوراثة سيكون كالتالي.

المثال الرابع

main.cpp
		#include <iostream>

		using namespace std;

		// printA يحتوي على دالة إسمها A هنا قمنا بتعريف كلاس إسمه
		class A {

		public: 
		void printA()
		{
		cout << "Hello from class A \n";
		}

		};

		// printB و يحتوي على دالة إسمها A يرث من الكلاس B هنا قمنا بتعريف كلاس إسمه
		class B : public A {

		public:
		void printB()
		{
		cout << "Hello from class B \n";
		}

		};

		// printC و يحتوي على دالة إسمها A يرث من الكلاس C هنا قمنا بتعريف كلاس إسمه
		class C: public A {

		public:
		void printC()
		{
		cout << "Hello from class C \n";
		}

		};

		// main() هنا قمنا بتعريف الدالة
		int main()
		{
		// و استدعاء جميع الدوال الموجودة فيه b إسمه B هنا قمنا بإنشاء كائن من الكلاس
		B b;
		b.printA();
		b.printB();

		// و استدعاء جميع الدوال الموجودة فيه c إسمه C هنا قمنا بإنشاء كائن من الكلاس
		C c;
		c.printA();
		c.printC();

		return 0;
		}
	  

سنحصل على النتيجة التالية عند التشغيل.

		Hello from class A
		Hello from class B
		Hello from class A
		Hello from class C
	  

مشكلة الوراثة المتعددة في C++

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


في المثال التالي قمنا بتعريف كلاس إسمه A يحتوي على دالة إسمها printMessage(), و كلاس إسمه B يحتوي على دالة إسمها printMessage() أيضاً.
بعدها قمنا بإنشاء كلاس إسمه C يرث من الكلاس A و الكلاس B.

إذاً هنا الكلاس C يرث دالة إسمها printMessage() من الكلاس A و دالة إسمها printMessage() من الكلاس B, و شكل الوراثة هو كالتالي.

بالمنطق, سيظهر لنا مشكلة عند محاولة إستدعاء الدالة printMessage() من الكلاس C لأن المترجم لن يعرف ما إذا كنا نريد استدعاء الدالة التي ورثها من الكلاس A أو التي ورثها من الكلاس B.

مثال

main.cpp
		#include <iostream>

		using namespace std;

		// printMessage يحتوي على دالة إسمها A هنا قمنا بتعريف كلاس إسمه
		class A {

		public: 
		void printMessage()
		{
		cout << "Hello from class A \n";
		}

		};

		// printMessage يحتوي على دالة إسمها B هنا قمنا بتعريف كلاس إسمه
		class B {

		public:
		void printMessage()
		{
		cout << "Hello from class A \n";
		}

		};

		// B و الكلاس A يرث من الكلاس C هنا قمنا بتعريف كلاس إسمه
		class C : public A, public B {

		};

		// main() هنا قمنا بتعريف الدالة
		int main()
		{
		// c إسمه C هنا قمنا بإنشاء كائن من الكلاس
		C c;

		// الذي يملك دالتين بهذا الإسم c من الكائن printMessage() هنا قمنا باستدعاء الدالة
		c.printMessage();

		return 0;
		}
	  

سيظهر لنا الخطأ التالي عند التشغيل.

error: request for member 'printMessage' is ambiguous|

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

C++ Encapsulation

مفهوم التغليف في C++

التغليف ( Encapsulation ) عبارة عن أسلوب يمكن اتباعه لإخفاء خصائص الكلاس ( Global Variables ) و جعل الكائنات التي تنشئها منه و الكلاسات الأخرى التي تقوم بتضمينه قادرة على التعامل مع هذه الخصائص فقط من خلال دوال يقوم بإنشائها المبرمج الأساسي للكلاس.

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


الأسلوب المتبع في عملية التغليف

بما أن فكرة التغليف الأساسية هي إخفاء البيانات من جهة و إتاحة التعامل معها من جهة أخرى.

أول ما يجب أن يخطر في بالك هو أنه يجب جعل نوع جميع الخصائص ( أي المتغيرات التي ستحفظ البيانات ) الموجودة في الكلاس private لأن تعريف جعلها private يعني أنه يمكن الوصول إليهم فقط من داخل الكلاس الموجودين فيه.

ثاني شيىء عليك التفكير فيه هو إيجاد طريقة للوصول إلى هذه الخصائص من الخارج.
لفعل ذلك عليك تجهيز دوال نوعها public للتعامل مع هذه الخصائص, لأن الدوال التي نوعها public يمكن الوصول إليهم من أي مكان.

إذاً لتحقيق مبدأ التغليف, عليك جعل نوع الخصائص private و جعل نوع الدوال التي تستخدم للوصول إليهم public.


مفهوم دوال الـ Setter و الـ Getter

عند التعامل مع أي متغير ( أو خاصية ) فعندك خيارين و هما إما إعطاءه قيمة جديدة و إما الحصول على القيمة الموجودة فيه.
بما أنه يجب بناء دوال للتعامل مع كل خاصية من الخصائص الموجودة في الكلاس, ينصح بإعتماد أسماء متعارف عليها كالتالي:

  • إبدأ إسم كل دالة الهدف منها إعطاء قيمة للخاصية بالكلمة set ثم إسم الخاصية.

  • إبدأ إسم كل دالة الهدف منها الحصول على قيمة الخاصية بالكلمة get ثم إسم الخاصية.

أمثلة شاملة حول التغليف في C++

الآن سنقوم بإنشاء كلاس إسمه Employee و فكرته تخزين معلومات الموظفين مثل الإسم name, الراتب salary, العمر age.
بعدها سنقوم بتجربة إنشاء كائن من الكلاس Employee في الدالة main() و التعامل معه.

ملاحظة: هنا قمنا بتعريف كل شيء بشكل عادي بدون تطبيق أي مبدأ من مبادئ التغليف.

المثال الأول

main.cpp
	  #include <iostream>

	  using namespace std;

	  // يحتوي على 3 خصائص Employee هنا قمنا بتعريف كلاس إسمه
	  class Employee {

	  public:
	  string name;       // لأنه عبارة عن نص string الإسم نوعه
	  int age;           // لأنه عبارة عن رقم int العمر نوعه
	  double salary;     // لأنه عبارة عن رقم كبير يمكن أن يحتوي على فاصلة double الراتب نوعه

	  };

	  // main() هنا قمنا بتعريف الدالة
	  int main()
	  {
	  // e إسمه Employee هنا قمنا بإنشاء كائن من الكلاس
	  Employee e;

	  // e هنا قمنا بإعطاء قيم لخصائص الكائن
	  e.name = "Mhamad";
	  e.age = 21;
	  e.salary = 950;

	  // e هنا قمنا بعرض قيم خصائص الكائن
	  cout << "Name: " << e.name << "\n";
	  cout << "Age: " << e.age << "\n";
	  cout << "Salary: " << e.salary;
	  }
	

سنحصل على النتيجة التالية عند التشغيل.

	  Name: Mhamad
	  Age: 21
	  Salary: 950
	


الآن سنقوم بإعادة المثال السابق مع جعل نوع الخصائص private و سنقوم بتعريف دوال نوعها public للتعامل مع هذه الخصائص.

ملاحظة: في هذا المثال قمنا بتطبيق مبدأ التغليف.

المثال الثاني

main.cpp
	  #include <iostream>

	  using namespace std;

	  // Employee هنا قمنا بتعريف الكلاس
	  class Employee {

	  // و بالتالي لم يعد ممكناً الوصول لهم بشكل مباشر من خارج الكلاس private لاحظ أننا جعلنا نوع الخصائص
	  private:
	  string name;
	  int age;
	  double salary;

	  // حتى نستطيع من الوصول من خارج الكلاس public هنا قمنا بتعريف جميع الدوال التي سنتعامل من خلالها مع الخصائص كـ
	  public:
	  // name هذه الدالة ترجع قيمة المخزنة الخاصية
	  string getName() {
	  return name;
	  }

	  // age هذه الدالة ترجع قيمة المخزنة الخاصية
	  int getAge() {
	  return age;
	  }

	  // salary هذه الدالة ترجع قيمة المخزنة الخاصية
	  double getSalary() {
	  return salary;
	  }

	  // name هذه الدالة نعطيها إسم فتقوم بوضعه للخاصية
	  void setName(string n) {
	  name = n;
	  }

	  // age هذه الدالة نعطيها رقم فتقوم بوضعه للخاصية
	  void setAge(int a) {
	  age = a;
	  }

	  // salary هذه الدالة نعطيها رقم فتقوم بوضعه للخاصية
	  void setSalary(double s) {
	  salary = s;
	  }
	  };

	  // main() هنا قمنا بتعريف الدالة
	  int main()
	  {
	  // e إسمه Employee هنا قمنا بإنشاء كائن من الكلاس
	  Employee e;

	  // Setter من خلال دوال الـ e هنا قمنا بوضع قيم لخصائص الكائن
	  e.setName("Mhamad");
	  e.setAge(21);
	  e.setSalary(950);

	  // Getter من خلال دوال الـ e هنا قمنا بعرض قيم خصائص الكائن
	  cout << "Name: " << e.getName() << "\n";
	  cout << "Age: " << e.getAge() << "\n";
	  cout << "Salary: " << e.getSalary();
	  }
	

سنحصل على النتيجة التالية عند التشغيل.

	  Name: Mhamad
	  Age: 21
	  Salary: 950
	


الآن سنعيد المثال السابق مع إضافة شرط على الدالة setName() يحدد أنه عند إدخال الإسم يجب أن يتألف من أكثر من حرفين.
و إضافة شرط على الدالة getName() لجعلها ترجع Not defined إذا كان لا يوجد إسم مدخل في الخاصية name.

ملاحظة: قمنا بتعليم الشروط و التعديلات الجديدة باللون الأصفر.

المثال الثالث

main.cpp
	  #include <iostream>

	  using namespace std;

	  // Employee هنا قمنا بتعريف الكلاس
	  class Employee {

	  // private لاحظ أننا أبقينا نوع الخصائص
	  private:
	  string name;
	  int age;
	  double salary;

	  public:
	  // name في حال لم يتم يكن هناك إسم مدخل في الخاصية "Not defined" لجعلها ترجع العبارة getName() هنا وضعنا شرط في الدالة
	  string getName() {
	  if (name == "")
	  {
	  return "Not defined";
	  }
	  else
	  {
	  return "Mr." + name;
	  }
	  }

	  // لم نجري أي تعديل على هذه الدالة
	  int getAge() {
	  return age;
	  }

	  // لم نجري أي تعديل على هذه الدالة
	  double getSalary() {
	  return salary;
	  }

	  // فقط في حال كان الإسم الذي نمرره لها يتألف من أكثر من حرفين name لجعلها تقبل تخزين الإسم في الخاصية setName() هنا وضعنا شرط في الدالة
	  void setName(string n) {
	  if (n.length() < 3)
	  {
	  cout << "Name is too short, name can't be less then 3 characters!\n";
	  }
	  else
	  {
	  name = n;
	  }
	  }

	  // لم نجري أي تعديل على هذه الدالة
	  void setAge(int a) {
	  age = a;
	  }

	  // age هذه الدالة نعطيها رقم فتقوم بوضعه للخاصية
	  void setSalary(double s) {
	  salary = s;
	  }
	  };

	  // main() هنا قمنا بتعريف الدالة
	  int main()
	  {
	  // e إسمه Employee هنا قمنا بإنشاء كائن من الكلاس
	  Employee e;

	  e.setName("dj");     // كإسم لأنه يتألف من حرفين فقط "dj" هنا لن يقبل أن ندخل
	  e.setAge(21);
	  e.setSalary(950);

	  cout << "Name: " << e.getName() << "\n";    // لأن الإسم الذي أدخلناه سابقاً لم يتم قبلوه "Not defined" هنا يفترضي أن ترجع الدالة عبارة
	  cout << "Age: " << e.getAge() << "\n";
	  cout << "Salary: " << e.getSalary();
	  }
	

سنحصل على النتيجة التالية عند التشغيل.

	  Name is too short, name can't be less then 3 characters!
	  Name: Not defined
	  Age: 21
	  Salary: 950
	

لاحظ أنه لم يقبل الإسم الذي أدخلناه عن طريق الدالة setName() لأنه أصغر من ثلاثة أحرف, لذلك طبع الرسالة التي قمنا بتجهيزها في حال تم إدخال إسم أصغر من ثلاثة أحرف.
و بما أنه لم يضع الإسم الذي قمنا بإدخاله في الخاصية name لاحظ أن الدالة getName() قامت بإرجاع القيمة Not defined التي قمنا بتجهيزها في حال لم يتم إدخال أي إسم فيه.