ما هو تعدد الأشكال؟ وما هي آلية تنفيذه؟ ما الفرق بين التحميل الزائد وإعادة الكتابة؟ هذه هي المفاهيم الأربعة المهمة جدًا التي سنراجعها هذه المرة: الميراث، وتعدد الأشكال، والتحميل الزائد، والكتابة الفوقية.
الميراث
ببساطة، الوراثة هي إنشاء نوع جديد يعتمد على نوع موجود عن طريق إضافة طرق جديدة أو إعادة تعريف الطرق الموجودة (كما هو موضح أدناه، تسمى هذه الطريقة إعادة الكتابة). يعد الوراثة إحدى الخصائص الأساسية الثلاث للكائنات الموجهة - التغليف والميراث وتعدد الأشكال. كل فئة نكتبها عند استخدام JAVA هي فئة موروثة، لأنه في لغة JAVA، تعد فئة java.lang.Object هي الفئة الأساسية الأكثر أهمية ( أو الفئة الأصلية أو الفئة الفائقة) لجميع الفئات إذا كانت الفئة المحددة حديثًا التي نحددها لا تحدد بشكل صريح الفئة الأساسية التي ترث منها، فستقوم JAVA بالوراثة من فئة الكائن بشكل افتراضي.
يمكننا تقسيم الفئات في JAVA إلى الأنواع الثلاثة التالية:
فئة: فئة محددة باستخدام فئة ولا تحتوي على أساليب مجردة.
فئة مجردة: فئة محددة باستخدام فئة مجردة، والتي قد تحتوي أو لا تحتوي على أساليب مجردة.
الواجهة: فئة محددة باستخدام الواجهة.
توجد قواعد الميراث التالية بين هذه الأنواع الثلاثة:
يمكن للفصول توسيع الفئات والفصول المجردة وتنفيذ الواجهات.
يمكن للفئات المجردة أن ترث (تمدد) الفئات، ويمكنها أن ترث (تمدد) الفئات المجردة، ويمكنها أن ترث (تنفذ) الواجهات.
يمكن للواجهات توسيع الواجهات فقط.
يرجى ملاحظة أنه لا يمكن استبدال الكلمات الرئيسية المختلفة والأدوات المستخدمة في كل حالة وراثة في القواعد الثلاثة المذكورة أعلاه حسب الرغبة. كما نعلم جميعًا، بعد أن ترث فئة عادية واجهة، يجب عليها تنفيذ جميع الأساليب المحددة في هذه الواجهة، وإلا فلا يمكن تعريفها إلا على أنها فئة مجردة. السبب وراء عدم استخدام مصطلح "التنفيذ" للكلمة الرئيسية التي يتم تنفيذها هنا هو أنه من الناحية المفاهيمية يمثل أيضًا علاقة وراثة، وفي حالة واجهة تنفيذ الفئة المجردة، لا يلزم تنفيذ تعريف الواجهة هذا على الإطلاق الطريقة، لذلك فمن المعقول أكثر لاستخدام الميراث.
تتوافق القواعد الثلاث المذكورة أعلاه أيضًا مع القيود التالية:
يمكن لكل من الفئات والفئات المجردة أن ترث فئة واحدة فقط على الأكثر، أو فئة مجردة واحدة على الأكثر، وهاتان الحالتان متنافيتان، أي أنهما إما يرثان فئة أو فئة مجردة.
عندما ترث الفئات والفئات المجردة والواجهات واجهات، فإنها غير مقيدة بالعدد، ومن الناحية النظرية، يمكنها أن ترث عددًا غير محدود من الواجهات. بالطبع، بالنسبة للفصل، يجب عليه تنفيذ جميع الأساليب المحددة في جميع الواجهات التي يرثها.
عندما ترث فئة مجردة فئة مجردة أو تنفذ واجهة، فقد لا تنفذ الأساليب المجردة جزئيًا أو كليًا أو كليًا للفئة المجردة الأصلية أو الواجهات المحددة في واجهة الفئة الأصلية.
عندما يرث الفصل فئة مجردة أو ينفذ واجهة، يجب عليه تنفيذ جميع الأساليب المجردة للفئة المجردة الأصلية أو جميع الواجهات المحددة في واجهة الفئة الأصلية.
الفائدة التي يجلبها الميراث لبرمجتنا هي إعادة استخدام (إعادة استخدام) الفئات الأصلية. تمامًا مثل إعادة استخدام الوحدات، يمكن أن تؤدي إعادة استخدام الفئات إلى تحسين كفاءة التطوير لدينا. في الواقع، فإن إعادة استخدام الوحدات هو التأثير المتراكب لإعادة استخدام عدد كبير من الفئات. بالإضافة إلى الميراث، يمكننا أيضًا استخدام التركيب لإعادة استخدام الفئات. ما يسمى بالتركيبة هو تحديد الفئة الأصلية كسمة للفئة الجديدة وتحقيق إعادة الاستخدام عن طريق استدعاء أساليب الفئة الأصلية في الفئة الجديدة. إذا لم تكن هناك علاقة مضمنة بين النوع المحدد حديثًا والنوع الأصلي، أي من مفهوم مجرد، فإن الأشياء التي يمثلها النوع المحدد حديثًا ليست من الأشياء التي يمثلها النوع الأصلي، مثل الأشخاص الأصفر إنه نوع من البشر، وهناك علاقة بينهما، بما في ذلك ويتم تضمينهما، لذلك في هذا الوقت، يعد الجمع خيارًا أفضل لتحقيق إعادة الاستخدام. المثال التالي هو مثال بسيط للجمع:
public class Sub { public Parent p = new Parent(); public void doSomething() { // إعادة استخدام الطريقة p.method() للفئة الرئيسية // رمز آخر } } class Parent { public void Method() { / / افعل شيئًا هنا } }
بالطبع، من أجل جعل الكود أكثر كفاءة، يمكننا أيضًا تهيئته عندما نحتاج إلى استخدام النوع الأصلي (مثل Parent p).
يعد استخدام الميراث والدمج لإعادة استخدام الفئات الأصلية نموذجًا للتطوير المتزايد. وتتمثل ميزة هذه الطريقة في عدم الحاجة إلى تعديل الكود الأصلي، لذلك لن يجلب أخطاء جديدة إلى الكود الأصلي، وليست هناك حاجة لذلك إعادة الاختبار بسبب التعديلات على الكود الأصلي، وهو أمر مفيد بشكل واضح لتطويرنا. لذلك، إذا كنا نقوم بصيانة أو تحويل نظام أو وحدة أصلية، خاصة عندما لا يكون لدينا فهم شامل لها، فيمكننا اختيار نموذج التطوير التزايدي، والذي لا يمكنه تحسين كفاءة التطوير لدينا بشكل كبير فحسب، بل أيضًا تجنب المخاطر الناجمة عن تعديلات على الكود الأصلي.
تعدد الأشكال
يعد تعدد الأشكال مفهومًا أساسيًا مهمًا آخر، كما ذكرنا سابقًا، فهو أحد الخصائص الأساسية الثلاثة للكائنات الموجهة. ما هو بالضبط تعدد الأشكال؟ دعونا نلقي نظرة أولاً على المثال التالي للمساعدة في الفهم:
// واجهة واجهة السيارة Car { // اسم السيارة String getName(); // احصل على سعر السيارة int getPrice() } // فئة BMW BMW Implements Car { public String getName() { return "BMW"; getPrice() { return 300000 } } // فئة CheryQQ CheryQQ Implements Car { public String getName() { return "CheryQQ" } public int getPrice(); { return 20000; } } // متجر مبيعات السيارات public class CarShop { // دخل مبيعات السيارات public int money = 0; + car.getName() + "سعر الوحدة:" + car.getPrice()); // زيادة الدخل من أموال مبيعات السيارات += car.getPrice() } // إجمالي الدخل من مبيعات السيارات public int getMoney() { return money } public static void main(String[] args) { CarShop aShop = new CarShop(); // بيع سيارة BMW aShop.sellCar(new BMW()); .sellCar(new CheryQQ()); System.out.println("إجمالي الإيرادات: " + aShop.getMoney());
نتائج التشغيل:
الموديل: BMW سعر الوحدة: 300.000
الموديل: شيري كيو كيو سعر الوحدة: 20.000
إجمالي الإيرادات: 320.000
الميراث هو الأساس لتحقيق تعدد الأشكال. من المفهوم حرفيًا، تعدد الأشكال هو نوع (كلا نوع السيارة) يظهر حالات متعددة (اسم BMW هو BMW والسعر 300000؛ اسم Chery هو CheryQQ والسعر 2000). يُطلق على ربط استدعاء الأسلوب بالموضوع (أي الكائن أو الفئة) الذي تنتمي إليه الطريقة اسم الربط، والذي ينقسم إلى نوعين: الربط المبكر والربط المتأخر. يتم شرح تعريفاتهم أدناه:
الربط المبكر: الربط قبل تشغيل البرنامج، ويتم تنفيذه بواسطة المترجم والرابط، ويسمى أيضًا الربط الثابت. على سبيل المثال، الطرق الثابتة والطرق النهائية لاحظ أن الطرق الخاصة مدرجة هنا أيضًا لأنها نهائية ضمنيًا.
الربط المتأخر: الربط وفقًا لنوع الكائن في وقت التشغيل، ويتم تنفيذه بواسطة آلية استدعاء الطريقة، لذلك يطلق عليه أيضًا الربط الديناميكي، أو ربط وقت التشغيل. كافة الأساليب باستثناء الربط المبكر هي ربط متأخر.
يتم تنفيذ تعدد الأشكال على آلية الارتباط المتأخر. الفائدة التي يجلبها لنا تعدد الأشكال هي أنه يلغي علاقة الاقتران بين الفئات ويجعل البرنامج أسهل في التوسع. على سبيل المثال، في المثال أعلاه، لإضافة نوع جديد من السيارات للبيع، ما عليك سوى السماح للفئة المحددة حديثًا بوراثة فئة السيارة وتنفيذ جميع أساليبها دون إجراء أي تعديلات على الكود الأصلي ) من طريقة فئة CarShop يمكنها التعامل مع موديلات السيارات الجديدة. الكود الجديد هو كما يلي:
// فئة سيارة سانتانا سانتانا تنفذ السيارة { public String getName() { return "Santana" } public int getPrice() { return 80000 } }
الحمولة الزائدة والتجاوز
التحميل الزائد وإعادة الكتابة هما مفهومان للطرق. قبل توضيح هذين المفهومين، دعونا نفهم أولاً ما هي بنية الطريقة (الاسم الإنجليزي هو التوقيع، ويتم ترجمة بعضها على أنها "توقيع"، على الرغم من أنها تستخدم على نطاق أوسع، ولكن هذه الترجمة ليست كذلك. دقيق). يشير الهيكل إلى بنية تكوين الطريقة، بما في ذلك على وجه التحديد اسم ومعلمات الطريقة، ويغطي عدد المعلمات ونوعها وترتيب ظهورها، ولكنه لا يتضمن نوع قيمة الإرجاع للطريقة ومعدلات الوصول والتعديلات مثل الرمز المجرد والثابت والنهائي. على سبيل المثال، الطريقتان التاليتان لهما نفس البنية:
طريقة الفراغ العامة (int i، String s) { // افعل شيئًا } طريقة السلسلة العامة (int i، String s) { // افعل شيئًا }
هاتان طريقتان بتكوينات مختلفة:
طريقة الفراغ العامة (int i, String s) { // افعل شيئًا } طريقة الفراغ العامة (String s, int i) { // افعل شيئًا }
بعد فهم مفهوم الجشطالت، دعونا نلقي نظرة على التحميل الزائد وإعادة الكتابة، يرجى إلقاء نظرة على تعريفاتها:
التجاوز، الاسم الإنجليزي هو التجاوز، يعني أنه في حالة الميراث، يتم تعريف طريقة جديدة لها نفس بنية الطريقة الموجودة في الفئة الأساسية في الفئة الفرعية، والتي تسمى الفئة الفرعية التي تتجاوز طريقة الفئة الأساسية. هذه خطوة ضرورية لتحقيق تعدد الأشكال.
التحميل الزائد، الاسم الإنجليزي هو التحميل الزائد، ويشير إلى تعريف أكثر من طريقة بنفس الاسم ولكن بهياكل مختلفة في نفس الفئة. لا يجوز في نفس الفئة تعريف أكثر من طريقة بنفس النوع.
دعونا نفكر في سؤال مثير للاهتمام: هل يمكن تحميل المنشئين فوق طاقتهم؟ الجواب بالطبع نعم، غالبًا ما نقوم بذلك في البرمجة الفعلية. في الواقع، اسم المُنشئ هو أيضًا طريقة، ومعلمات المُنشئ هي معلمات الطريقة، وقيمة الإرجاع هي مثيل للفئة التي تم إنشاؤها حديثًا. ومع ذلك، لا يمكن تجاوز المنشئ بواسطة فئة فرعية، لأن الفئة الفرعية لا يمكنها تعريف مُنشئ بنفس نوع الفئة الأساسية.
التحميل الزائد والتجاوز وتعدد الأشكال وإخفاء الوظائف
غالبًا ما يُلاحظ أن بعض المبتدئين في لغة C++ لديهم فهم غامض للتحميل الزائد والكتابة الفوقية وتعدد الأشكال وإخفاء الوظائف. سأكتب بعض آرائي الخاصة هنا، على أمل مساعدة المبتدئين في لغة C++ على إزالة شكوكهم.
قبل أن نفهم العلاقة المعقدة والدقيقة بين التحميل الزائد والكتابة الفوقية وتعدد الأشكال وإخفاء الوظائف، يجب علينا أولاً مراجعة المفاهيم الأساسية مثل التحميل الزائد والتغطية.
أولاً، دعونا نلقي نظرة على مثال بسيط جدًا لفهم ماهية إخفاء الوظيفة.
#include <iostream>استخدام مساحة الاسم std;class Base{public: void fun() { cout << "Base::fun()" << endl }};class Derive : public Base{public: void fun(int i ) { cout << "Derive::fun()" << endl }};int main(){ Derive d; // الجملة التالية خاطئة لذا تم حظرها //d.fun();error C2660 : 'fun' : الدالة لا تأخذ 0 معلمات d.fun(1); Derive *pd =new Derive(); // الجملة التالية خاطئة، لذا تم حظرها //pd->fun();error C2660: 'fun': لا تأخذ الدالة 0 معلمات pd->fun(1);
/*لا تشكل الوظائف في نطاقات مختلفة غير مساحة الاسم تحميلاً زائدًا، والفئات الفرعية والفئات الأصلية هما نطاقان مختلفان.
في هذا المثال، توجد الوظيفتان في نطاقات مختلفة، لذا لا يتم تحميلهما بشكل زائد إلا إذا كان النطاق عبارة عن نطاق مساحة اسم. */
في هذا المثال، لم يتم تحميل الوظيفة بشكل زائد أو تجاوزها، ولكنها مخفية.
تشرح الأمثلة الخمسة التالية على وجه التحديد ما هو الإخفاء.
مثال 1
#include <iostream>استخدام مساحة الاسم std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}// التحميل الزائد void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Deriv :public Basic{public: void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();// صحيح، لا تحتوي الفئة المشتقة على تعريف دالة بنفس اسم الفئة الأساسية، ثم جميع الوظائف المحملة بنفس الاسم سيتم استخدام الفئة الأساسية كوظائف مرشحة. d.fun(1);// صحيح، لا تحتوي الفئة المشتقة على إعلان دالة بنفس اسم الفئة الأساسية، ثم سيتم استخدام جميع الوظائف المحملة بشكل زائد بنفس الاسم في الفئة الأساسية كوظائف مرشحة. العودة 0؛}
مثال 2
#include <iostream>استخدام مساحة الاسم std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}// التحميل الزائد void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Deriv :public Basic{public: // إصدار وظيفة جديد، تم حظر جميع الإصدارات المحملة بشكل زائد من الفئة الأساسية، وهنا نسميها إخفاء وظيفة إخفاء // إذا كان هناك إعلان لوظيفة بنفس اسم الفئة الأساسية في الفئة المشتقة، فلن يتم استخدام الوظيفة التي تحمل نفس الاسم في الفئة الأساسية كوظيفة مرشحة، حتى لو كانت الفئة الأساسية تحتوي على إصدارات متعددة من الوظائف المحملة بشكل زائد مع قوائم المعلمات المختلفة. متعة باطلة (int i،int j) {cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl ;}};int main(){ اشتقاق d.fun(1,2); // الجملة التالية خاطئة لذا تم حظرها //d.fun();خطأ C2660: 'fun': الوظيفة تعمل لا تأخذ 0 المعلمات العودة 0؛}
مثال 3
#include <iostream>استخدام مساحة الاسم std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}// التحميل الزائد void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: // تجاوز أحد إصدارات الوظائف من فئة التجاوز الأساسية. وبالمثل، فإن جميع الإصدارات المحملة بشكل زائد من الفئة الأساسية هي مختفي. // إذا كان هناك إعلان لوظيفة بنفس اسم الفئة الأساسية في الفئة المشتقة، فلن يتم استخدام الوظيفة التي تحمل نفس الاسم في الفئة الأساسية كوظيفة مرشحة، حتى لو كانت الفئة الأساسية تحتوي على إصدارات متعددة من الوظائف المحملة بشكل زائد مع قوائم المعلمات المختلفة. باطلة fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ اشتقاق d; d.fun(); // الجملة التالية خاطئة، لذا تم حظرها //d.fun(1);خطأ C2660: 'fun': الدالة لا تأخذ معلمة واحدة return 0;}
مثال 4
#include <iostream>استخدام مساحة الاسم std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}// التحميل الزائد void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Deriv :public Basic{public: use Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d.fun();// صحيح d.fun(1); // الإرجاع الصحيح 0;}/*اشتقاق نتيجة الإخراج::fun()Base::fun(int i)اضغط على أي مفتاح للمتابعة*/
مثال 5
#include <iostream>استخدام مساحة الاسم std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}// التحميل الزائد void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Deriv :public Basic{public: use Basic::fun; void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d .fun();// صحيح d.fun(1);// صحيح d.fun(1,2);// الإرجاع الصحيح 0;}/* نتيجة الإخراج Base::fun()Base::fun(int i)Derive::fun(int i,int j)اضغط على أي مفتاح للمتابعة*/
حسنًا، لنقم أولاً بعمل ملخص صغير للخصائص بين التحميل الزائد والكتابة الفوقية.
خصائص الزائد:
ن نفس النطاق (في نفس الفئة)؛
n اسم الوظيفة هو نفسه ولكن المعلمات مختلفة؛
n الكلمة الأساسية الافتراضية اختيارية.
التجاوز يعني أن وظيفة الفئة المشتقة تغطي وظيفة الفئة الأساسية وخصائص التجاوز هي:
نطاقات مختلفة (تقع في الفئات المشتقة والفئات الأساسية على التوالي)؛
n اسم الوظيفة ومعلماتها متماثلان؛
n يجب أن تحتوي وظائف الفئة الأساسية على الكلمة الأساسية الافتراضية. (إذا لم تكن هناك كلمة رئيسية افتراضية، يطلق عليها إخفاء مخفي)
إذا كانت الفئة الأساسية تحتوي على عدة إصدارات مثقلة من إحدى الوظائف، وقمت بتجاوز (تجاوز) واحد أو أكثر من إصدارات الوظائف في الفئة الأساسية في الفئة المشتقة، أو إضافة إصدارات جديدة في إصدار وظيفة الفئة المشتقة (نفس اسم الوظيفة، ومعلمات مختلفة) ، فسيتم حظر جميع الإصدارات المحملة بشكل زائد من الفئة الأساسية، والتي نسميها مخفية هنا. لذلك، بشكل عام، عندما تريد استخدام إصدار دالة جديد في فئة مشتقة وترغب في استخدام إصدار دالة من الفئة الأساسية، يجب عليك تجاوز جميع الإصدارات المحملة بشكل زائد في الفئة الأساسية في الفئة المشتقة. إذا كنت لا ترغب في تجاوز إصدار الوظيفة المحملة بشكل زائد للفئة الأساسية، فيجب عليك استخدام المثال 4 أو المثال 5 للإعلان بشكل صريح عن نطاق مساحة اسم الفئة الأساسية.
في الواقع، يعتقد مترجم C++ أنه لا توجد علاقة بين الوظائف التي لها نفس اسم الوظيفة والمعلمات المختلفة، فهما ببساطة وظيفتان غير مرتبطتين. كل ما في الأمر أن لغة C++ قدمت مفاهيم التحميل الزائد والكتابة الفوقية من أجل محاكاة العالم الحقيقي والسماح للمبرمجين بالتعامل مع مشكلات العالم الحقيقي بشكل أكثر حدسية. يقع التحميل الزائد ضمن نطاق مساحة الاسم نفسه، بينما يكون التجاوز ضمن نطاقات مساحة اسم مختلفة. على سبيل المثال، تعد الفئة الأساسية والفئة المشتقة نطاقين مختلفين لمساحة الاسم. أثناء عملية الميراث، إذا كانت الفئة المشتقة لها نفس اسم وظيفة الفئة الأساسية، فسيتم إخفاء وظيفة الفئة الأساسية. بالطبع، الموقف الذي تمت مناقشته هنا هو عدم وجود كلمة رئيسية افتراضية أمام وظيفة الفئة الأساسية. سنناقش الموقف عندما تكون هناك كلمة رئيسية افتراضية بشكل منفصل.
تتجاوز الفئة الموروثة إصدار دالة من الفئة الأساسية لإنشاء واجهة وظيفية خاصة بها. في الوقت الحالي، يعتقد مترجم C++ أنه نظرًا لأنك تريد الآن استخدام الواجهة المعاد كتابتها للفئة المشتقة، فلن يتم توفير واجهة الفئة الأساسية الخاصة بي لك (بالطبع يمكنك استخدام طريقة الإعلان بوضوح عن نطاق مساحة الاسم، راجع [أساسيات C++] التحميل الزائد والكتابة الفوقية وتعدد الأشكال وإخفاء الوظائف (1)). إنه يتجاهل أن واجهة فئتك الأساسية لها خصائص التحميل الزائد. إذا كنت ترغب في الاستمرار في الحفاظ على ميزة التحميل الزائد في الفئة المشتقة، فقم بإعطاء ميزة التحميل الزائد للواجهة بنفسك. لذلك، في فئة مشتقة، طالما أن اسم الوظيفة هو نفسه، سيتم حظر إصدار الوظيفة من الفئة الأساسية بلا رحمة. في المترجم، يتم تنفيذ الإخفاء من خلال نطاق مساحة الاسم.
لذلك، للحفاظ على الإصدار المحمل بشكل زائد من وظيفة الفئة الأساسية في الفئة المشتقة، يجب عليك تجاوز كافة الإصدارات المحملة بشكل زائد من الفئة الأساسية. التحميل الزائد صالح فقط في الفصل الحالي، وسيفقد الميراث خصائص التحميل الزائد للوظيفة. بمعنى آخر، إذا كنت تريد وضع الوظيفة المحملة بشكل زائد للفئة الأساسية في الفئة المشتقة الموروثة، فيجب عليك إعادة كتابتها.
تعني كلمة "مخفي" هنا أن وظيفة الفئة المشتقة تحظر وظيفة الفئة الأساسية التي تحمل الاسم نفسه، فلنقدم أيضًا ملخصًا موجزًا للقواعد المحددة:
n إذا كانت وظيفة الفئة المشتقة لها نفس اسم وظيفة الفئة الأساسية، ولكن المعلمات مختلفة. في هذا الوقت، إذا لم تكن الفئة الأساسية تحتوي على الكلمة الأساسية الافتراضية، فسيتم إخفاء وظائف الفئة الأساسية. (احرص على عدم الخلط بينه وبين التحميل الزائد. على الرغم من أن الوظيفة التي تحمل نفس الاسم ومعلمات مختلفة يجب أن تسمى التحميل الزائد، إلا أنه لا يمكن فهمها على أنها تحميل زائد هنا لأن الفئة المشتقة والفئة الأساسية ليسا في نفس نطاق مساحة الاسم. هذا هو يفهم على أنه مختبئ)
n إذا كانت وظيفة الفئة المشتقة لها نفس اسم وظيفة الفئة الأساسية، ولكن المعلمات مختلفة. في هذا الوقت، إذا كانت الفئة الأساسية تحتوي على الكلمة الأساسية الافتراضية، فسيتم توريث وظائف الفئة الأساسية ضمنيًا في جدول vtable الخاص بالفئة المشتقة. في هذا الوقت، تشير الوظيفة الموجودة في فئة vtable المشتقة إلى عنوان الوظيفة لإصدار الفئة الأساسية. وفي الوقت نفسه، تتم إضافة إصدار الوظيفة الجديد هذا إلى الفئة المشتقة كإصدار مثقل من الفئة المشتقة. ولكن عندما يطبق مؤشر الفئة الأساسية طريقة وظيفة الاتصال متعددة الأشكال، سيتم إخفاء إصدار وظيفة الفئة المشتقة الجديدة.
n إذا كانت وظيفة الفئة المشتقة لها نفس اسم وظيفة الفئة الأساسية، وكانت المعلمات هي أيضًا نفسها، ولكن وظيفة الفئة الأساسية لا تحتوي على الكلمة الأساسية الافتراضية. في هذا الوقت، يتم إخفاء وظائف الفئة الأساسية. (احذر من الخلط بينه وبين التغطية، التي يفهم منها هنا الإخفاء).
n إذا كانت وظيفة الفئة المشتقة لها نفس اسم وظيفة الفئة الأساسية، وكانت المعلمات هي أيضًا نفسها، ولكن وظيفة الفئة الأساسية لها الكلمة الأساسية الافتراضية. في هذا الوقت، لن تكون وظائف الفئة الأساسية "مخفية". (هنا عليك أن تفهمها على أنها تغطية ^_^).
فاصل: عندما لا تكون هناك كلمة رئيسية افتراضية أمام وظيفة الفئة الأساسية، نحتاج إلى إعادة كتابتها بشكل أكثر سلاسة. عندما تكون هناك كلمة رئيسية افتراضية، فمن المنطقي أن نسميها تجاوزًا يمكن للجميع فهم C++ بشكل أفضل. وبدون المزيد من اللغط، دعونا نوضح بمثال.
مثال 6
#include <iostream>استخدام مساحة الاسم std; class Base{public: virtual void fun() { cout << "Base::fun()" << endl }//overload virtual void fun(int i) { cout << "Base::fun(int i)" << endl }//overload}; class Derive : public Base{public: void fun() { cout << "Derive::fun()" << endl; }//تجاوز void fun(int i) { cout << "Derive::fun(int i)" << endl }//override void fun(int i,int j){ cout<< "Derive::fun; (int i,int j)" <<endl;}//overload}; int main(){ Base *pb = new Derive(); pb->fun(); pb->fun(1); // الجملة التالية خاطئة، لذا تم حظرها //pb->fun(1,2); لا يمكن تحميل الوظيفة الافتراضية بشكل زائد، الخطأ C2661: 'fun': لا تأخذ الوظيفة المحملة بشكل زائد معلمتين cout << endl; pd = new Derive();
نتائج الإخراج
اشتقاق::متعة()
اشتقاق::fun(int i)
اشتقاق::متعة()
اشتقاق::fun(int i)
اشتقاق::fun(int i,int j)
اضغط على أي مفتاح للمتابعة
*/
مثال 7-1
# تضمين <iostream> باستخدام مساحة الاسم std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; int main(){ Base *pb = new Derive();//Base::fun(int i) حذف pb;
مثال 7-2
# تضمين <iostream> باستخدام مساحة الاسم std؛ class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(double d){ cout <<"Derive::fun(double d)"<< endl } }; pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) حذف pb;}
مثال 8-1
# تضمين <iostream> باستخدام مساحة الاسم std؛ class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl }}; pb->fun(1);//Derive::fun(int i) حذف pb;
مثال 8-2
# تضمين <iostream> باستخدام مساحة الاسم std؛ class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((double) 0.01);//اشتقاق::fun(int i) حذف pb;
مثال 9
# تضمين <iostream> باستخدام مساحة الاسم std؛ class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }};class Deriv : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< endl } void fun(char c){ cout <<"Derive::fun(char c)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl } };int main(){ Base *pb = new Derive(); //اشتقاق::fun(int i) pb->fun('a');//اشتق::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive();//Derive::fun(int i) // التحميل الزائد pd->fun('a');//مشتق::fun(char c) //التحميل الزائد pd->fun(0.01);//Derive::fun(double d) حذف pb؛ حذف pd؛ }
من السهل فهم الأمثلة 7-1 و8-1، وقد وضعت هذين المثالين هنا ليتمكن الجميع من المقارنة ولمساعدة الجميع على الفهم بشكل أفضل:
n في المثال 7-1، لا تغطي الفئة المشتقة الوظيفة الافتراضية للفئة الأساسية. في هذا الوقت، يكون العنوان الذي يشير إليه مؤشر الوظيفة في جدول vtable للفئة المشتقة هو عنوان الوظيفة الافتراضية للفئة الأساسية.
n في المثال 8-1، تتجاوز الفئة المشتقة الوظيفة الافتراضية للفئة الأساسية. في هذا الوقت، يكون العنوان الذي يشير إليه مؤشر الوظيفة في جدول vtable للفئة المشتقة هو عنوان الوظيفة الافتراضية التي تم تجاوزها للفئة المشتقة.
تبدو الأمثلة 7-2 و8-2 غريبة بعض الشيء، في الواقع، إذا قارنتها وفقًا للمبادئ المذكورة أعلاه، فستكون الإجابة واضحة:
في المثال 7-2، قمنا بتحميل إصدار دالة للفئة المشتقة بشكل زائد: void fun(double d) في الواقع، هذا مجرد غطاء. دعنا نحللها على وجه التحديد. هناك العديد من الوظائف في الفئة الأساسية والعديد من الوظائف في الفئة المشتقة:
اكتب الفئة الأساسية المشتقة
قسم Vtable
متعة باطلة (int i)
يشير إلى إصدار الفئة الأساسية للوظيفة الافتراضية void fun(int i)
جزء ثابت
متعة باطلة (د مزدوج)
دعونا نحلل الأسطر الثلاثة التالية من التعليمات البرمجية مرة أخرى:
Base *pb = new Derive();
pb->fun(1);//القاعدة::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
الجملة الأولى هي المفتاح، يشير مؤشر الفئة الأساسية إلى كائن الفئة المشتقة نوع كائن وقت التشغيل، لذا انتقل أولاً إلى جدول vtable للفئة المشتقة للعثور على إصدار الوظيفة الافتراضية للفئة المشتقة، وقد وجد أن الفئة المشتقة لا تغطي الوظيفة الافتراضية للفئة الأساسية تقوم الفئة المشتقة فقط بإعداد مؤشر إلى عنوان الوظيفة الافتراضية للفئة الأساسية، لذلك من الطبيعي استدعاء إصدار الفئة الأساسية للوظيفة الافتراضية. في الجملة الأخيرة، لا يزال البرنامج يبحث عن جدول vtable للفئة المشتقة، ويجد أنه لا توجد وظيفة افتراضية لهذا الإصدار على الإطلاق، لذلك عليه العودة واستدعاء الوظيفة الافتراضية الخاصة به.
ومن الجدير بالذكر هنا أيضًا أنه إذا كانت الفئة الأساسية تحتوي على وظائف افتراضية متعددة في هذا الوقت، فسيقوم البرنامج بمطالبة "مكالمة غير واضحة" عند تجميع البرنامج. الأمثلة هي كما يلي
#include <iostream> باستخدام مساحة الاسم std؛ class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl } virtual void fun(char c){ cout < <"Base::fun(char c)"<< endl }}; class Derive : public Base{public: void fun(double d){ cout <<"Derive::fun(double) d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);// خطأ C2668: 'fun': استدعاء غامض للوظيفة المحملة بشكل زائد حذف pb; إرجاع 0;}
حسنًا، دعونا نحلل المثال 8-2 مرة أخرى.
في المثال 8-2، قمنا أيضًا بتحميل إصدار دالة للفئة المشتقة بشكل زائد: void fun(double d)، وقمنا أيضًا بتغطية الوظيفة الافتراضية للفئة الأساسية، دعنا نحللها بالتفصيل والفئة المشتقة لها عدة وظائف. للفئة عدة وظائف:
اكتب الفئة الأساسية المشتقة
قسم Vtable
متعة باطلة (int i)
متعة باطلة (int i)
جزء ثابت
متعة باطلة (د مزدوج)
من الجدول، يمكننا أن نرى أن مؤشر الوظيفة في جدول vtable للفئة المشتقة يشير إلى عنوان الوظيفة الافتراضية الخاص به والذي تم تجاوزه.
دعونا نحلل الأسطر الثلاثة التالية من التعليمات البرمجية مرة أخرى:
Base *pb = new Derive();
pb->fun(1);//اشتقاق::fun(int i)
pb->fun((double)0.01);//اشتقاق::fun(int i)
ليست هناك حاجة لقول المزيد عن الجملة الأولى، فالجملة الثانية هي استدعاء نسخة الوظيفة الافتراضية للفئة المشتقة بطبيعة الحال. الجملة الثالثة، تبدو غريبة مرة أخرى غبي أثناء التشغيل، انغمس في جدول vtable للفئة المشتقة، لقد نظرت إليه للتو وأدركت أنه لا يوجد إصدار أريده حقًا. لماذا لا تنظر مؤشرات الطبقة الأساسية حولها وتبحث عنها؟ لقد تبين أن بصرها محدود. جزء Vtable (أي الجزء الثابت) وجزء Vtable الذي تريد إدارته، فراغ الفئة المشتقة fun(double d) بعيدة جدًا، ولا يمكنك رؤيتها! بالإضافة إلى ذلك، يجب على الفئة المشتقة أن تعتني بكل شيء، أليس لدى الفئة المشتقة بعض القوة الخاصة بها؟ مهلا، لا مزيد من الجدل، يمكن للجميع ذلك اعتنوا بأنفسهم ^_^
للأسف، هل ستتنهد؟ يمكن لمؤشر الفئة الأساسية إجراء مكالمات متعددة الأشكال، لكنه لا يمكنه أبدًا إجراء مكالمات مثقلة بالفئات المشتقة (راجع المثال 6)~~~
دعونا ننظر إلى المثال 9 مرة أخرى،
تأثير هذا المثال هو نفس تأثير المثال 6، ولنفس الغرض. أعتقد أنه بعد فهم الأمثلة المذكورة أعلاه، فهذه أيضًا قبلة صغيرة.
ملخص:
يحدد التحميل الزائد إصدار الوظيفة المطلوب استدعاؤه بناءً على قائمة معلمات الوظيفة، بينما يحدد تعدد الأشكال إصدار الوظيفة الافتراضية المراد استدعاؤه بناءً على النوع الفعلي لكائن وقت التشغيل، ويتم تنفيذ تعدد الأشكال من خلال الفئات المشتقة إلى الفئات الأساسية الافتراضية يتم تنفيذ الوظيفة عن طريق تجاوزها، إذا لم تتجاوز الفئة المشتقة الوظيفة الافتراضية الافتراضية للفئة الأساسية، فسترث الفئة المشتقة الوظيفة الافتراضية الافتراضية للفئة الأساسية تلقائيًا. إصدار الوظيفة في هذا الوقت، بغض النظر عما إذا كان الكائن الذي يشير إليه مؤشر الفئة الأساسية هو نوع أساسي أو نوع مشتق، فسيتم استدعاء الوظيفة الافتراضية لإصدار الفئة الأساسية إذا تجاوزت الفئة المشتقة الوظيفة الافتراضية سيتم استدعاء الفئة الأساسية في وقت التشغيل وفقًا للنوع الفعلي للكائن الذي سيتم استخدامه لتحديد إصدار الوظيفة الافتراضية التي سيتم استدعاؤها، على سبيل المثال، إذا كان نوع الكائن الذي يشير إليه مؤشر الفئة الأساسية هو نوع مشتق سيتم استدعاء نسخة الوظيفة الافتراضية الافتراضية للفئة المشتقة، وبالتالي تحقيق تعدد الأشكال.
الهدف الأصلي من استخدام تعدد الأشكال هو الإعلان عن أن الوظيفة افتراضية في الفئة الأساسية، وتجاوز إصدار الوظيفة الافتراضية الافتراضية للفئة الأساسية في الفئة المشتقة. لاحظ أن النموذج الأولي للوظيفة في هذا الوقت يتوافق مع الفئة الأساسية. أي نفس الاسم ونوع المعلمة؛ إذا قمت بإضافة إصدار وظيفة جديد إلى الفئة المشتقة، فلا يمكنك استدعاء إصدار الوظيفة الجديد للفئة المشتقة ديناميكيًا من خلال مؤشر الفئة الأساسية كإصدار مثقل من الفئة المشتقة. لا تزال نفس الجملة، التحميل الزائد صالح فقط في الفصل الحالي سواء قمت بتحميله بشكل زائد في فئة أساسية أو فئة مشتقة، فإن الاثنين غير مرتبطين ببعضهما البعض. إذا فهمنا ذلك، فيمكننا أيضًا فهم نتائج المخرجات في المثالين 6 و9 بنجاح.
التحميل الزائد مرتبط بشكل ثابت، وتعدد الأشكال مرتبط ديناميكيًا. لمزيد من التوضيح، لا علاقة للتحميل الزائد بنوع الكائن الذي يشير إليه المؤشر بالفعل، ويرتبط تعدد الأشكال بنوع الكائن الذي يشير إليه المؤشر بالفعل. إذا استدعى مؤشر الفئة الأساسية إصدارًا محمّلاً بشكل زائد من فئة مشتقة، فإن مترجم C++ يعتبره غير قانوني، ويعتقد مترجم C++ فقط أن مؤشر الفئة الأساسية يمكنه فقط استدعاء الإصدار المحمّل بشكل زائد من الفئة الأساسية، وأن التحميل الزائد يعمل فقط في مساحة الاسم. الفئة الحالية الصالحة داخل المجال، ستفقد الميراث ميزة التحميل الزائد بالطبع، إذا استدعى مؤشر الفئة الأساسية وظيفة افتراضية. ثم سيتم أيضًا تحديد إصدار الوظيفة الافتراضية الافتراضية للفئة الأساسية ديناميكيًا أو إصدار الوظيفة الافتراضية الافتراضية للفئة المشتقة لتنفيذ عمليات محددة. يتم تحديد ذلك من خلال نوع الكائن الذي يشير إليه مؤشر الفئة الأساسية بالفعل، وبالتالي التحميل الزائد والمؤشرات إن نوع الكائن الذي يشير إليه المؤشر فعليًا لا علاقة له به؛ ويرتبط تعدد الأشكال بنوع الكائن الذي يشير إليه المؤشر بالفعل.
أخيرًا، للتوضيح، يمكن أيضًا تحميل الوظائف الافتراضية بشكل زائد، لكن التحميل الزائد لا يمكن أن يكون فعالاً إلا في نطاق مساحة الاسم الحالية. ما هو عدد كائنات السلسلة التي تم إنشاؤها؟
دعونا نلقي نظرة أولاً على جزء من الكود:
كود جافا
String str=new String("abc");
غالبًا ما يتبع هذا الرمز سؤال، كم عدد كائنات السلسلة التي تم إنشاؤها بواسطة هذا السطر من التعليمات البرمجية؟ أعتقد أن هذا السؤال يعرفه الجميع، والإجابة معروفة، 2. بعد ذلك، سنبدأ من هذا السؤال ونراجع بعض المعرفة المتعلقة بـ JAVA المتعلقة بإنشاء كائنات السلسلة.
يمكننا تقسيم السطر أعلاه من التعليمات البرمجية إلى أربعة أجزاء: String str و= و"abc" وnew String(). تحدد String فقط متغير نوع السلسلة المسمى str، لذلك لا تنشئ كائنًا؛ = تهيئة المتغير str وتعيين مرجع (أو مقبض) لكائن له، ومن الواضح أنه لا ينشئ كائنًا؛ الآن فقط جديد تم ترك السلسلة ("abc"). فلماذا يمكن اعتبار السلسلة الجديدة ("abc") بمثابة "abc" وسلسلة جديدة ()؟ دعونا نلقي نظرة على منشئ السلسلة الذي أطلقنا عليه:
كود جافا
سلسلة عامة (سلسلة أصلية) {
// كود آخر ...
}
كما نعلم جميعًا، هناك طريقتان شائعتان الاستخدام لإنشاء مثيلات (كائنات) للفئة:
استخدم الجديد لإنشاء الكائنات.
اتصل بطريقة newInstance لفئة Class واستخدم آلية الانعكاس لإنشاء الكائن.
استخدمنا new لاستدعاء طريقة المنشئ المذكورة أعلاه لفئة String لإنشاء كائن وتعيين مرجعه للمتغير str. في الوقت نفسه، لاحظنا أن المعلمة التي تقبلها طريقة المُنشئ المُستدعى هي أيضًا كائن سلسلة، وهذا الكائن هو بالضبط "abc". من هذا علينا أن نقدم طريقة أخرى لإنشاء كائن سلسلة - النص الموجود بين علامات الاقتباس.
هذه الطريقة فريدة بالنسبة للسلسلة، وهي مختلفة جدًا عن الطريقة الجديدة.
كود جافا
سلسلة str = "abc"؛
ليس هناك شك في أن هذا السطر من التعليمات البرمجية ينشئ كائن سلسلة.
كود جافا
سلسلة = "اي بي سي"؛
سلسلة ب = "اي بي سي"؛
ماذا عن هنا؟ الجواب لا يزال واحدا.
كود جافا
سلسلة = "أب"+"قرص مضغوط"؛
ماذا عن هنا؟ الجواب لا يزال واحدا. غريب بعض الشيء؟ في هذه المرحلة، نحتاج إلى تقديم مراجعة للمعرفة المتعلقة بتجمع السلاسل.
يوجد تجمع سلسلة في جهاز JAVA الظاهري (JVM)، والذي يخزن العديد من كائنات السلسلة ويمكن مشاركته، لذلك فهو يحسن الكفاءة. نظرًا لأن فئة السلسلة نهائية، فلا يمكن تغيير قيمتها بمجرد إنشائها، لذلك لا داعي للقلق بشأن ارتباك البرنامج الناتج عن مشاركة كائنات السلسلة. يتم الحفاظ على تجمع السلسلة بواسطة فئة السلسلة، ويمكننا استدعاء طريقة intern () للوصول إلى تجمع السلسلة.
دعونا نلقي نظرة على String a = "abc"؛. عند تنفيذ هذا السطر من التعليمات البرمجية، يبحث جهاز JAVA الظاهري أولاً في تجمع السلاسل لمعرفة ما إذا كان هذا الكائن ذو القيمة "abc" موجودًا بالفعل أم لا القيمة المرجعة لفئة السلسلة تساوي طريقة (Object obj). إذا كان هناك، فلن يتم إنشاء كائن جديد، وسيتم إرجاع مرجع إلى الكائن الموجود مباشرة؛ وإذا لم يكن الأمر كذلك، فسيتم إنشاء الكائن أولاً، ثم إضافته إلى تجمع السلسلة، ثم سيتم إرجاع مرجعه. ولذلك، ليس من الصعب علينا أن نفهم لماذا يحتوي المثالان الأولان من الأمثلة الثلاثة السابقة على هذه الإجابة.
بالنسبة للمثال الثالث:
كود جافا
سلسلة = "أب"+"قرص مضغوط"؛
لأن قيمة الثابت يتم تحديدها في وقت الترجمة. هنا، "ab" و"cd" ثابتان، لذلك يمكن تحديد قيمة المتغير a في وقت الترجمة. التأثير المترجم لهذا السطر من التعليمات البرمجية يعادل:
كود جافا
سلسلة = "abcd"؛
لذلك، يتم إنشاء كائن واحد فقط "abcd" هنا، ويتم حفظه في تجمع السلسلة.
الآن يأتي السؤال مرة أخرى، هل سيتم إضافة جميع السلاسل التي تم الحصول عليها بعد الاتصال "+" إلى تجمع السلاسل؟ نعلم جميعًا أنه يمكن استخدام "==" للمقارنة بين متغيرين وله الحالتين التاليتين:
إذا تمت مقارنة نوعين أساسيين (char، byte، short، int، long، float، double، boolean)، يتم الحكم على ما إذا كانت قيمهما متساوية.
إذا قارن الجدول متغيرين كائنين، فسيتم الحكم على ما إذا كانت مراجعهما تشير إلى نفس الكائن.
بعد ذلك سوف نستخدم "==" لإجراء بعض الاختبارات. ولتسهيل التوضيح، نعتبر الإشارة إلى كائن موجود بالفعل في تجمع السلاسل بمثابة الكائن الذي تتم إضافته إلى تجمع السلاسل:
كود جافا
public class StringTest { public static void main(String[] args) { String a = "ab";// أنشئ كائنًا وأضفه إلى تجمع السلاسل System.out.println("String a = /"ab/" ; "); String b = "cd";// يتم إنشاء كائن وإضافته إلى تجمع السلاسل System.out.println("String b = /"cd/";"); String c = "abcd"; // يتم إنشاء كائن وإضافته إلى تجمع السلاسل String d = "ab" + "cd"; // إذا كان d وc يشيران إلى نفس الكائن، فهذا يعني أنه تمت إضافة d أيضًا إلى تجمع السلاسل if (d == c ) { System.out.println("/"ab/"+/"cd/" تتم إضافة الكائن الذي تم إنشاؤه/" إلى/" تجمع السلسلة" } // إذا لم يشير d و c إلى نفس الشيء كائن، فهذا يعني أنه لم تتم إضافة d إلى تجمع السلسلة else { System.out.println("/"ab/"+/"cd/" الكائن الذي تم إنشاؤه/"لم تتم إضافته/" إلى تجمع السلسلة"); } String e = a + "cd"; c يشير إلى نفس الكائن، فهذا يعني أنه تمت إضافة e أيضًا إلى تجمع السلسلة if (e == c) { System.out.println(" a +/"cd/" الكائن الذي تم إنشاؤه/سلسلة "انضم/" تجمع الأوسط"); } // إذا لم يشير e وc إلى نفس الكائن، فهذا يعني أنه لم تتم إضافة e إلى تجمع السلسلة else { System.out.println(" a +/"cd/" كائن تم إنشاؤه/"لم تتم إضافته/" إلى تجمع السلسلة" ); } String f = "ab" + b; // إذا كان f وc يشيران إلى نفس الكائن، فهذا يعني أنه تمت إضافة f أيضًا إلى تجمع السلسلة if (f == c) { System.out .println("/ كائن تم إنشاؤه بواسطة "ab/"+ b /"أضيف/" إلى تجمع السلاسل"); } // إذا لم يشير f وc إلى نفس الكائن، فهذا يعني أنه لم تتم إضافة f إلى تجمع السلاسل else { System.out.println("/" ab/" + b كائن تم إنشاؤه/"لم تتم إضافته/" إلى تجمع السلسلة"); } String g = a + b; // إذا كان g وc يشيران إلى نفس الكائن، فهذا يعني أنه تمت إضافة g أيضًا إلى تجمع السلسلة if ( g == c) { System.out.println("a + ب الكائن الذي تم إنشاؤه/"أضيف/" إلى تجمع السلسلة")؛ } // إذا لم يشير g وc إلى نفس الكائن، فهذا يعني أنه لم تتم إضافة g إلى تجمع السلسلة else { System.out.println ("كائن تم إنشاؤه a + b/"لم تتم إضافته/" إلى تجمع السلسلة");
نتائج التشغيل هي كما يلي:
سلسلة أ = "أب"؛
سلسلة ب = "قرص مضغوط"؛
الكائن الذي تم إنشاؤه بواسطة "ab"+"cd" "منضم" إلى تجمع السلسلة
الكائن الذي تم إنشاؤه بواسطة + "cd" "لا تتم إضافته" إلى تجمع السلسلة.
الكائن الذي تم إنشاؤه بواسطة "ab"+ b "لم تتم إضافته" إلى تجمع السلسلة.
الكائن الذي تم إنشاؤه بواسطة a + b "لا تتم إضافته" إلى تجمع السلسلة من النتائج المذكورة أعلاه، يمكننا أن نرى بسهولة أنه سيتم إضافة الكائنات الجديدة التي تم إنشاؤها باستخدام اتصالات "+" بين كائنات السلسلة التي تم إنشاؤها باستخدام علامات الاقتباس لتضمين النص. في تجمع السلسلة. بالنسبة لجميع تعبيرات الاتصال "+" التي تحتوي على كائنات تم إنشاؤها في الوضع الجديد (بما في ذلك القيمة الخالية)، لن تتم إضافة الكائنات الجديدة التي تنشئها إلى تجمع السلاسل، ولن نخوض في تفاصيل حول هذا الأمر.
ولكن هناك حالة واحدة تتطلب اهتمامنا. يرجى إلقاء نظرة على الكود أدناه:
كود جافا
public class StringStaticTest { // Constant A public static Final String A = "ab"; // Constant B public static Final String B = "cd"; لتهيئة s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s يساوي t، هما نفس الكائن" } else { System.out.println("s لا تساوي t، فهي ليست نفس الكائن");
نتيجة تشغيل هذا الكود هي كما يلي:
s يساوي t وهما نفس الكائن لماذا؟ والسبب هو أن قيمة الثابت ثابتة، لذلك يمكن تحديدها في وقت الترجمة، في حين لا يمكن تحديد قيمة المتغير إلا في وقت التشغيل، لأنه يمكن استدعاء هذا المتغير بطرق مختلفة، لذلك قد يتسبب في حدوث ذلك. قيمة للتغيير. في المثال أعلاه، A و B ثابتان وقيمتهما ثابتة، وبالتالي فإن قيمة s ثابتة أيضًا، ويتم تحديدها عند تجميع الفصل. وهذا يعني:
كود جافا
سلسلة ق = أ + ب؛
يعادل:
كود جافا
سلسلة s = "ab"+"cd"؛
اسمحوا لي أن أغير المثال أعلاه قليلاً وأرى ما سيحدث:
كود جافا
public class StringStaticTest { // Constant A public static Final String A; // Constant B public static Final String B; // تهيئة s من خلال ربط الثوابتين بـ + String s = A + B String t = "abcd"; System.out.println("s يساوي t، هما نفس الكائن"); } else { System.out.println("s لا يساوي t، فهما ليسا نفس الكائن");
نتيجة عملها هي كما يلي:
s لا يساوي t، وهما ليسا نفس الكائن ولكن تم تعديلهما قليلاً، والنتيجة هي عكس المثال تمامًا الآن. دعونا نحللها مرة أخرى. على الرغم من تعريف A وB كثوابت (لا يمكن تعيينهما إلا مرة واحدة)، إلا أنه لا يتم تعيينهما على الفور. قبل أن يتم حساب قيمة s، متى يتم تخصيصها والقيمة المخصصة لها كلها متغيرات. لذلك، يتصرف A وB كمتغير قبل تعيين قيمة له. ثم لا يمكن تحديد s في وقت الترجمة، ولكن يمكن إنشاؤها فقط في وقت التشغيل.
نظرًا لأن مشاركة الكائنات في تجمع السلاسل يمكن أن تحسن الكفاءة، فإننا نشجع الجميع على إنشاء كائنات سلسلة من خلال تضمين النص بين علامتي الاقتباس. في الواقع، هذا هو ما نستخدمه غالبًا في البرمجة.
بعد ذلك، دعونا نلقي نظرة على طريقة intern()، والتي تم تعريفها على النحو التالي:
كود جافا
سلسلة أصلية عامة () ؛
هذه طريقة أصلية. عند استدعاء هذه الطريقة، يتحقق جهاز JAVA الظاهري أولاً مما إذا كان الكائن الذي له قيمة مساوية للكائن موجود بالفعل في تجمع السلسلة. إذا كان الأمر كذلك، فإنه يُرجع مرجعًا إلى الكائن في تجمع السلسلة، وإذا لم يكن كذلك، فإنه يقوم أولاً بإنشاء ملف كائن في تجمع السلسلة بنفس القيمة، ثم يقوم بإرجاع المرجع الخاص به.
دعونا نلقي نظرة على هذا الرمز:
كود جافا
public class StringInternTest { public static void main(String[] args) { // استخدم مصفوفة char لتهيئة a لتجنب وجود كائن بقيمة "abcd" بالفعل في تجمع السلسلة قبل إنشاء a String a = new String ( new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); System.out.println("تمت إضافة b إلى تجمع السلاسل، ولم يتم إنشاء كائن جديد"); } else { System.out.println("لم تتم إضافة b إلى تجمع السلاسل، ولم يتم إنشاء كائن جديد"); } } }
نتائج التشغيل:
لم تتم إضافة b إلى تجمع السلسلة وتم إنشاء كائن جديد. إذا لم تجد طريقة intern() لفئة السلسلة كائنًا له نفس القيمة، فإنها تضيف الكائن الحالي إلى تجمع السلسلة ثم تُرجعه. مرجع، ثم يشير b وa إلى نفس الكائن، وإلا فسيتم إنشاء الكائن المشار إليه بواسطة b حديثًا بواسطة جهاز JAVA الظاهري في تجمع السلاسل، لكن قيمته هي نفسها a. تؤكد نتيجة تشغيل الكود أعلاه هذه النقطة فقط.
أخيرًا، دعونا نتحدث عن تخزين كائنات السلسلة في جهاز JAVA الظاهري (JVM)، والعلاقة بين تجمع السلسلة والكومة والمكدس. دعونا أولاً نراجع الفرق بين الكومة والمكدس:
المكدس: يحفظ بشكل أساسي الأنواع الأساسية (أو الأنواع المضمنة) (char، byte، short، int، long، float، double، boolean) ويمكن مشاركة بيانات الكائنات، وتأتي سرعتها في المرتبة الثانية بعد التسجيل كومة.
الكومة: تستخدم لتخزين الأشياء.
عندما ننظر إلى الكود المصدري لفئة السلسلة، سنجد أنه يحتوي على سمة قيمة، والتي تخزن قيمة كائن السلسلة، والنوع هو char[]، والذي يوضح أيضًا أن السلسلة عبارة عن سلسلة من الأحرف.
عند تنفيذ String a = "abc"؛، يقوم جهاز JAVA الظاهري بإنشاء ثلاث قيم أحرف "a" و"b" و"c" في المكدس، ثم يقوم بإنشاء كائن سلسلة في الكومة، قيمته (القيمة) عبارة عن مصفوفة من ثلاث قيم أحرف تم إنشاؤها للتو على المكدس {'a'، 'b'، 'c'}. وأخيرًا، سيتم إضافة كائن السلسلة الذي تم إنشاؤه حديثًا إلى تجمع السلاسل. إذا قمنا بعد ذلك بتنفيذ الكود String b=new String("abc"); منذ إنشاء "abc" وحفظه في تجمع السلاسل، فسيقوم جهاز JAVA الظاهري بإنشاء كائن سلسلة جديد فقط في الكومة، ولكنه القيمة هي قيم أنواع الأحرف الثلاثة "a" و"b" و"c" التي تم إنشاؤها على المكدس عند تنفيذ السطر السابق من التعليمات البرمجية.
في هذه المرحلة، أصبحنا واضحين تمامًا بشأن السؤال عن سبب قيام String str=new String("abc") المرفوعة في بداية هذه المقالة بإنشاء كائنين.