ความแตกต่างคืออะไร? กลไกการดำเนินการคืออะไร? อะไรคือความแตกต่างระหว่างการโอเวอร์โหลดและการเขียนใหม่? ต่อไปนี้เป็นแนวคิดที่สำคัญมากสี่ประการที่เราจะทบทวนในครั้งนี้: การสืบทอด ความหลากหลาย การโอเวอร์โหลด และการเขียนทับ
มรดก
พูดง่ายๆ ก็คือ การสืบทอดคือการสร้างประเภทใหม่ตามประเภทที่มีอยู่โดยการเพิ่มวิธีการใหม่หรือกำหนดวิธีการที่มีอยู่ใหม่ (ดังที่อธิบายไว้ด้านล่าง วิธีการนี้เรียกว่าการเขียนใหม่) การสืบทอดเป็นหนึ่งในสามคุณลักษณะพื้นฐานของเชิงวัตถุ - การห่อหุ้ม การสืบทอด และความหลากหลาย ทุกคลาสที่เราเขียนเมื่อใช้ JAVA คือการสืบทอด เนื่องจากในภาษา JAVA คลาส java.lang.Object นั้นเป็นคลาสพื้นฐานที่สุด ( หรือคลาสพาเรนต์หรือคลาสซุปเปอร์) ของคลาสทั้งหมด หากคลาสที่กำหนดใหม่ที่เรากำหนดไม่ได้ระบุอย่างชัดเจนว่าคลาสพื้นฐานนั้นสืบทอดมาจากคลาสใด JAVA จะใช้ค่าเริ่มต้นเป็นคลาสที่สืบทอดจากคลาส Object
เราสามารถแบ่งคลาสใน JAVA ออกเป็นสามประเภทดังต่อไปนี้:
คลาส: คลาสที่กำหนดโดยใช้คลาสและไม่มีเมธอดนามธรรม
คลาสนามธรรม: คลาสที่กำหนดโดยใช้คลาสนามธรรม ซึ่งอาจมีหรือไม่มีวิธีนามธรรม
อินเทอร์เฟซ: คลาสที่กำหนดโดยใช้อินเทอร์เฟซ
กฎการสืบทอดต่อไปนี้มีอยู่ระหว่างสามประเภทนี้:
คลาสสามารถขยายคลาส คลาสนามธรรม และปรับใช้อินเทอร์เฟซ
คลาสนามธรรมสามารถสืบทอด (ขยาย) คลาส พวกเขาสามารถสืบทอด (ขยาย) คลาสนามธรรม และสามารถสืบทอด (นำไปใช้) อินเทอร์เฟซ
อินเทอร์เฟซสามารถขยายอินเทอร์เฟซได้เท่านั้น
โปรดทราบว่าคำหลักที่แตกต่างกันจะขยายและนำไปใช้ในแต่ละกรณีการสืบทอดในกฎสามข้อข้างต้นไม่สามารถถูกแทนที่ได้ตามต้องการ ดังที่เราทุกคนทราบกันดีว่า หลังจากที่คลาสธรรมดาสืบทอดอินเทอร์เฟซแล้ว จะต้องใช้วิธีการทั้งหมดที่กำหนดไว้ในอินเทอร์เฟซนี้ มิฉะนั้นจะสามารถกำหนดให้เป็นคลาสนามธรรมเท่านั้น เหตุผลที่ฉันไม่ใช้คำว่า "การนำไปปฏิบัติ" สำหรับคีย์เวิร์ด Implement ที่นี่ก็เนื่องมาจากตามแนวคิดแล้วมันแสดงถึงความสัมพันธ์ที่สืบทอดมาด้วย และในกรณีของอินเทอร์เฟซคลาสนามธรรมนำไปใช้ ก็ไม่จำเป็นต้องใช้คำจำกัดความของอินเทอร์เฟซนี้ ดังนั้นจึงมีความสมเหตุสมผลมากกว่าที่จะใช้การสืบทอด
กฎสามข้อข้างต้นยังเป็นไปตามข้อจำกัดต่อไปนี้:
ทั้งคลาสและคลาสนามธรรมสามารถสืบทอดได้มากที่สุดเพียงคลาสเดียว หรือคลาสนามธรรมได้มากที่สุดเพียงคลาสเดียวเท่านั้น และทั้งสองสถานการณ์นี้จะไม่เกิดร่วมกัน กล่าวคือ ทั้งสองคลาสจะสืบทอดคลาสหรือคลาสนามธรรมก็ได้
เมื่อคลาส คลาสนามธรรม และอินเทอร์เฟซสืบทอดอินเทอร์เฟซ พวกมันไม่ถูกจำกัดด้วยจำนวน ตามทฤษฎี พวกมันสามารถสืบทอดอินเทอร์เฟซได้ไม่จำกัดจำนวน แน่นอนว่าสำหรับคลาสนั้นจะต้องใช้วิธีการทั้งหมดที่กำหนดไว้ในอินเทอร์เฟซทั้งหมดที่สืบทอดมา
เมื่อคลาสนามธรรมสืบทอดคลาสนามธรรมหรือใช้อินเทอร์เฟซ มันอาจไม่ใช้วิธีการนามธรรมของคลาสนามธรรมพาเรนต์บางส่วน สมบูรณ์หรือทั้งหมด หรืออินเทอร์เฟซที่กำหนดไว้ในอินเทอร์เฟซคลาสพาเรนต์
เมื่อคลาสสืบทอดคลาสนามธรรมหรือใช้อินเทอร์เฟซ จะต้องใช้วิธีการนามธรรมทั้งหมดของคลาสนามธรรมพาเรนต์หรืออินเทอร์เฟซทั้งหมดที่กำหนดไว้ในอินเทอร์เฟซคลาสพาเรนต์
ประโยชน์ที่การสืบทอดนำมาสู่การเขียนโปรแกรมของเราคือการนำกลับมาใช้ใหม่ (ใช้ซ้ำ) ของคลาสดั้งเดิม เช่นเดียวกับการนำโมดูลกลับมาใช้ใหม่ การใช้คลาสซ้ำสามารถปรับปรุงประสิทธิภาพการพัฒนาของเราได้ จริงๆ แล้วการนำโมดูลกลับมาใช้ใหม่เป็นผลที่ทับซ้อนกันของการนำคลาสจำนวนมากกลับมาใช้ใหม่ นอกจากการสืบทอดแล้ว เรายังสามารถใช้การเรียบเรียงเพื่อนำคลาสกลับมาใช้ใหม่ได้อีกด้วย การรวมกันที่เรียกว่าคือการกำหนดคลาสดั้งเดิมเป็นคุณลักษณะของคลาสใหม่และบรรลุการนำกลับมาใช้ใหม่โดยการเรียกเมธอดของคลาสดั้งเดิมในคลาสใหม่ หากไม่มีความสัมพันธ์ระหว่างประเภทที่กำหนดใหม่กับประเภทดั้งเดิม กล่าวคือ จากแนวคิดเชิงนามธรรม สิ่งที่แสดงโดยประเภทที่กำหนดใหม่ไม่ใช่สิ่งหนึ่งซึ่งแสดงโดยประเภทดั้งเดิม เช่น คนสีเหลือง มันเป็นมนุษย์ประเภทหนึ่งและมีความสัมพันธ์ระหว่างพวกเขา การรวมและการถูกรวมเข้าด้วยกัน ดังนั้นในเวลานี้ การรวมกันจึงเป็นทางเลือกที่ดีกว่าในการนำกลับมาใช้ใหม่ ตัวอย่างต่อไปนี้เป็นตัวอย่างง่ายๆ ของการรวมกัน:
คลาสสาธารณะ { ไพรเวทพาเรนต์ p = คลาสพาเรนต์ใหม่ () { // ใช้เมธอด p.method () ของคลาสพาเรนต์ซ้ำ // รหัสอื่น ๆ } } คลาสพาเรนต์ { วิธีโมฆะสาธารณะ () { / / ทำบางอย่างที่นี่ } }
แน่นอน เพื่อทำให้โค้ดมีประสิทธิภาพมากขึ้น เรายังสามารถเริ่มต้นมันได้เมื่อเราจำเป็นต้องใช้ประเภทดั้งเดิม (เช่น Parent p)
การใช้การสืบทอดและการรวมกันเพื่อนำคลาสดั้งเดิมกลับมาใช้ใหม่ถือเป็นรูปแบบการพัฒนาแบบเพิ่มหน่วย ข้อดีของวิธีนี้คือไม่จำเป็นต้องแก้ไขโค้ดต้นฉบับ ดังนั้นจึงไม่นำข้อบกพร่องใหม่มาสู่โค้ดต้นฉบับ และไม่จำเป็นต้องแก้ไข ทดสอบใหม่เนื่องจากมีการแก้ไขโค้ดต้นฉบับซึ่งเป็นประโยชน์ต่อการพัฒนาของเราอย่างเห็นได้ชัด ดังนั้น หากเรากำลังรักษาหรือเปลี่ยนแปลงระบบหรือโมดูลดั้งเดิม โดยเฉพาะอย่างยิ่งเมื่อเราไม่มีความเข้าใจอย่างถ่องแท้ เราสามารถเลือกรูปแบบการพัฒนาแบบค่อยเป็นค่อยไป ซึ่งไม่เพียงแต่สามารถปรับปรุงประสิทธิภาพการพัฒนาของเราได้อย่างมาก แต่ยังหลีกเลี่ยงความเสี่ยงที่เกิดจาก การปรับเปลี่ยนรหัสเดิม
ความแตกต่าง
ความหลากหลายเป็นแนวคิดพื้นฐานที่สำคัญอีกประการหนึ่ง ดังที่ได้กล่าวไว้ข้างต้น มันเป็นหนึ่งในสามลักษณะพื้นฐานของการมุ่งเน้นเชิงวัตถุ ความแตกต่างคืออะไรกันแน่? ขั้นแรกเรามาดูตัวอย่างต่อไปนี้เพื่อช่วยให้เข้าใจ:
// อินเทอร์เฟซสำหรับรถ รถยนต์ { // ชื่อรถ String getName() { return "BMW" } public int getPrice() { return 300000; } } // CheryQQ คลาส CheryQQ ใช้ Car { public String getName() { return "CheryQQ" } public int getPrice() { คืน 20,000; } } // ร้านขายรถยนต์คลาสสาธารณะ CarShop { // รายได้จากการขายรถยนต์ส่วนตัว int เงิน = 0; // ขายรถยนต์สาธารณะขายรถยนต์ (รถยนต์) { System.out.println ("รุ่นรถ: " + car.getName() + " Unit price: " + car.getPrice()); // เพิ่มรายได้จากการขายรถยนต์ += car.getPrice(); } // รวมรายได้จากการขายรถยนต์ int getMoney() { คืนเงิน; } public static void main(String[] args) { CarShop aShop = new CarShop(); // ขาย BMW aShop.sellCar(ใหม่ BMW()); // ขาย BMW Chery QQ aShop .sellCar(ใหม่ CheryQQ()); System.out.println("รายได้รวม: " + aShop.getMoney());
ผลการวิ่ง:
รุ่น : BMW ราคาต่อหน่วย : 300,000
รุ่น : CheryQQ ราคาต่อหน่วย : 20,000
รายได้รวม: 320,000
การสืบทอดเป็นพื้นฐานสำหรับการตระหนักถึงความหลากหลาย เข้าใจตามตัวอักษร polymorphism เป็นประเภท (ประเภทรถยนต์ทั้งสอง) ที่แสดงหลายรัฐ (ชื่อ BMW คือ BMW และราคา 300,000 ชื่อ Chery คือ CheryQQ และราคา 2,000) การเชื่อมโยงการเรียกเมธอดกับหัวเรื่อง (นั่นคือ อ็อบเจ็กต์หรือคลาส) ซึ่งมีเมธอดอยู่เรียกว่า การรวม ซึ่งแบ่งออกเป็นสองประเภท: การรวมตั้งแต่เนิ่นๆ และการรวมภายหลัง คำจำกัดความของพวกเขาอธิบายไว้ด้านล่าง:
การผูกล่วงหน้า: การผูกก่อนที่โปรแกรมจะรัน ใช้งานโดยคอมไพลเลอร์และตัวเชื่อมโยง หรือที่เรียกว่าการผูกแบบคงที่ ตัวอย่างเช่น วิธีการแบบคงที่และวิธีสุดท้าย โปรดทราบว่าวิธีการส่วนตัวก็รวมอยู่ที่นี่ด้วยเนื่องจากเป็นวิธีสุดท้ายโดยปริยาย
การเชื่อมโยงล่าช้า: การเชื่อมโยงตามประเภทของอ็อบเจ็กต์ ณ รันไทม์ ใช้งานโดยกลไกการเรียกเมธอด ดังนั้นจึงเรียกว่าการเชื่อมโยงแบบไดนามิกหรือการเชื่อมโยงรันไทม์ วิธีการทั้งหมดยกเว้นการผูกล่วงหน้าถือเป็นการผูกล่าช้า
Polymorphism ถูกนำมาใช้กับกลไกของการผูกล่าช้า ประโยชน์ที่ความหลากหลายนำมาให้เราก็คือ ขจัดความสัมพันธ์ระหว่างคลาสต่างๆ และทำให้โปรแกรมขยายได้ง่ายขึ้น ตัวอย่างเช่น ในตัวอย่างข้างต้น ในการเพิ่มประเภทใหม่ของรถยนต์สำหรับการขาย คุณเพียงแค่ต้องปล่อยให้คลาสที่กำหนดใหม่สืบทอดคลาส Car และใช้วิธีการทั้งหมดโดยไม่ต้องทำการแก้ไขใดๆ กับโค้ดต้นฉบับ นั่นคือ sellCar(Car car ) ของคลาส CarShop สามารถรองรับรถยนต์รุ่นใหม่ได้ รหัสใหม่มีดังนี้:
// Santana Car class Santana ใช้ Car { 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) { // ทำบางสิ่งบางอย่าง }
หลังจากทำความเข้าใจแนวคิดของเกสตัลท์แล้ว เรามาดูคำจำกัดความของการโอเวอร์โหลดและการเขียนใหม่กันดีกว่า
การเอาชนะ ชื่อภาษาอังกฤษคือการแทนที่ หมายความว่าในกรณีของการสืบทอด วิธีการใหม่ที่มีโครงสร้างเดียวกันกับวิธีการในคลาสพื้นฐานถูกกำหนดไว้ในคลาสย่อย ซึ่งเรียกว่าคลาสย่อยที่เอาชนะวิธีการของคลาสฐาน นี่เป็นขั้นตอนที่จำเป็นเพื่อให้เกิดความหลากหลาย
Overloading ชื่อภาษาอังกฤษคือ Overloading หมายถึง การกำหนดวิธีการมากกว่าหนึ่งวิธีด้วยชื่อเดียวกัน แต่มีโครงสร้างต่างกันในคลาสเดียวกัน ในคลาสเดียวกัน ไม่อนุญาตให้กำหนดวิธีประเภทเดียวกันมากกว่าหนึ่งวิธี
ลองพิจารณาคำถามที่น่าสนใจ: ตัวสร้างสามารถบรรทุกเกินพิกัดได้หรือไม่? คำตอบคือใช่ เรามักจะทำเช่นนี้ในการเขียนโปรแกรมจริง ในความเป็นจริง Constructor ก็เป็นวิธีการเช่นกัน ชื่อ Constructor คือชื่อวิธีการ พารามิเตอร์ Constructor คือพารามิเตอร์ของวิธีการ และค่าที่ส่งคืนเป็นอินสแตนซ์ของคลาสที่สร้างขึ้นใหม่ อย่างไรก็ตาม ตัวสร้างไม่สามารถถูกแทนที่โดยคลาสย่อยได้ เนื่องจากคลาสย่อยไม่สามารถกำหนดตัวสร้างที่มีประเภทเดียวกันกับคลาสพื้นฐานได้
การโอเวอร์โหลด การเอาชนะ ความหลากหลายและการซ่อนฟังก์ชัน
บ่อยครั้งจะเห็นได้ว่าผู้เริ่มต้นใช้งาน C++ บางคนมีความเข้าใจที่คลุมเครือเกี่ยวกับการโอเวอร์โหลด การเขียนทับ ความหลากหลาย และการซ่อนฟังก์ชัน ฉันจะเขียนความคิดเห็นของฉันเองที่นี่ โดยหวังว่าจะช่วยให้ผู้เริ่มต้น C++ คลายข้อสงสัยได้
ก่อนที่เราจะเข้าใจความสัมพันธ์ที่ซับซ้อนและละเอียดอ่อนระหว่างการโอเวอร์โหลด การเขียนทับ ความหลากหลายและการซ่อนฟังก์ชัน เราต้องทบทวนแนวคิดพื้นฐานก่อน เช่น การโอเวอร์โหลดและการครอบคลุม
ขั้นแรก มาดูตัวอย่างง่ายๆ เพื่อทำความเข้าใจว่าการซ่อนฟังก์ชันคืออะไร
#include <iostream>โดยใช้ namespace 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: 'สนุก' : ฟังก์ชั่นไม่ได้ใช้พารามิเตอร์ 0 pd->fun(1); ลบ pd;
/*ฟังก์ชันในขอบเขตที่ไม่ใช่เนมสเปซที่แตกต่างกันไม่ก่อให้เกิดการโอเวอร์โหลด คลาสย่อยและคลาสพาเรนต์เป็นสองขอบเขตที่แตกต่างกัน
ในตัวอย่างนี้ ฟังก์ชันทั้งสองอยู่ในขอบเขตที่แตกต่างกัน ดังนั้นจึงไม่มีการโอเวอร์โหลด เว้นแต่ขอบเขตจะเป็นขอบเขตเนมสเปซ -
ในตัวอย่างนี้ ฟังก์ชันไม่ได้โอเวอร์โหลดหรือถูกแทนที่ แต่ถูกซ่อนไว้
ห้าตัวอย่างถัดไปจะอธิบายอย่างเจาะจงว่าการซ่อนคืออะไร
ตัวอย่างที่ 1
#include <iostream>โดยใช้ namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();//ถูกต้อง คลาสที่ได้รับไม่มีการประกาศฟังก์ชันที่มีชื่อเดียวกันกับคลาสพื้นฐาน ดังนั้นฟังก์ชันโอเวอร์โหลดทั้งหมดที่มีชื่อเดียวกันใน คลาสพื้นฐานจะถูกใช้เป็นฟังก์ชันผู้สมัคร d.fun(1);//ถูกต้อง คลาสที่ได้รับไม่มีการประกาศฟังก์ชันที่มีชื่อเดียวกันกับคลาสพื้นฐาน จากนั้นฟังก์ชันที่โอเวอร์โหลดทั้งหมดที่มีชื่อเดียวกันในคลาสฐานจะถูกใช้เป็นฟังก์ชันผู้สมัคร กลับ 0;}
ตัวอย่างที่ 2
#include <iostream>โดยใช้ namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //New function version, เวอร์ชันที่โอเวอร์โหลดทั้งหมดของคลาสพื้นฐานจะถูกบล็อก ในที่นี้เราเรียกว่าซ่อนฟังก์ชันซ่อน //หากมีการประกาศฟังก์ชันที่มีชื่อเดียวกันของคลาสฐานในคลาสที่ได้รับ ฟังก์ชันที่มีชื่อเดียวกันในคลาสฐานจะไม่ถูกใช้เป็นฟังก์ชันผู้สมัคร แม้ว่าคลาสฐานจะมีหลายเวอร์ชันก็ตาม ของฟังก์ชันโอเวอร์โหลดที่มีรายการพารามิเตอร์ต่างกัน เป็นโมฆะสนุก (int i,int j) {cout << "ได้รับ :: fun (int i, int j)" << endl;} เป็นโมฆะ fun2 () {cout << "ได้รับ :: fun2 ()" << endl ;}};int main(){ Derive d; d.fun(1,2); //ประโยคต่อไปนี้ผิด ดังนั้นจึงถูกบล็อก //d.fun();error C2660: 'fun' : function does ไม่ใช้พารามิเตอร์ 0 กลับ 0;}
ตัวอย่างที่ 3
#include <iostream>โดยใช้ namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Override หนึ่งในเวอร์ชันฟังก์ชันของคลาสฐานแทนที่ เช่นเดียวกัน คลาสฐานเวอร์ชันโอเวอร์โหลดทั้งหมดจะถูก ที่ซ่อนอยู่. //หากมีการประกาศฟังก์ชันที่มีชื่อเดียวกันของคลาสฐานในคลาสที่ได้รับ ฟังก์ชันที่มีชื่อเดียวกันในคลาสฐานจะไม่ถูกใช้เป็นฟังก์ชันผู้สมัคร แม้ว่าคลาสฐานจะมีหลายเวอร์ชันก็ตาม ของฟังก์ชันโอเวอร์โหลดที่มีรายการพารามิเตอร์ต่างกัน เป็นโมฆะสนุก () {cout << "ได้รับ :: fun ()" << endl;} เป็นโมฆะ fun2 () {cout << "ได้รับ:: fun2 ()" << endl;}}; int main () { ได้รับมา d; d.fun(); //ประโยคต่อไปนี้ผิด ดังนั้นจึงถูกบล็อก //d.fun(1);error C2660: 'fun' : ฟังก์ชั่นไม่ใช้ 1 พารามิเตอร์ return 0;}
ตัวอย่างที่ 4
#include <iostream>โดยใช้ namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: การใช้ Basic::fun; void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun();//แก้ไข d.fun(1); //แก้ไข return 0;}/*ผลลัพธ์ผลลัพธ์ Derive::fun()Base::fun(int i)กดปุ่มใดก็ได้เพื่อดำเนินการต่อ*/
ตัวอย่างที่ 5
#include <iostream>โดยใช้ namespace std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//overload void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: การใช้ Basic::fun; void fun(int i,int j){cout << "สืบทอด::fun(int i,int j)" << endl;} void fun2(){cout << "สืบทอด::fun2()" << endl;}};int main(){ สืบทอด 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 คำหลักเสมือนเป็นทางเลือก
Override หมายความว่าฟังก์ชันคลาสที่ได้รับจะครอบคลุมฟังก์ชันคลาสพื้นฐาน
n ขอบเขตที่แตกต่างกัน (อยู่ในคลาสที่ได้รับและคลาสพื้นฐานตามลำดับ);
n ชื่อฟังก์ชันและพารามิเตอร์เหมือนกัน
n ฟังก์ชันคลาสพื้นฐานต้องมีคีย์เวิร์ดเสมือน (หากไม่มีคีย์เวิร์ดเสมือน เรียกว่า ซ่อนซ่อน)
หากคลาสพื้นฐานมีฟังก์ชันโอเวอร์โหลดหลายเวอร์ชัน และคุณแทนที่ (แทนที่) เวอร์ชันฟังก์ชันอย่างน้อยหนึ่งเวอร์ชันในคลาสพื้นฐานในคลาสที่ได้รับ หรือเพิ่มเวอร์ชันใหม่ในเวอร์ชันฟังก์ชันคลาสที่ได้รับ (ชื่อฟังก์ชันเดียวกัน พารามิเตอร์ต่างกัน) จากนั้นคลาสพื้นฐานเวอร์ชันโอเวอร์โหลดทั้งหมดจะถูกบล็อก ซึ่งเราเรียกว่าซ่อนอยู่ที่นี่ ดังนั้น โดยทั่วไป เมื่อคุณต้องการใช้เวอร์ชันฟังก์ชันใหม่ในคลาสที่ได้รับ และต้องการใช้เวอร์ชันฟังก์ชันของคลาสพื้นฐาน คุณควรแทนที่เวอร์ชันที่โอเวอร์โหลดทั้งหมดในคลาสพื้นฐานในคลาสที่ได้รับ หากคุณไม่ต้องการแทนที่เวอร์ชันฟังก์ชันโอเวอร์โหลดของคลาสฐาน คุณควรใช้ตัวอย่างที่ 4 หรือตัวอย่างที่ 5 เพื่อประกาศขอบเขตเนมสเปซคลาสฐานอย่างชัดเจน
ในความเป็นจริง คอมไพเลอร์ C++ เชื่อว่าไม่มีความสัมพันธ์ระหว่างฟังก์ชันที่มีชื่อฟังก์ชันเดียวกันและพารามิเตอร์ที่แตกต่างกัน เป็นเพียงสองฟังก์ชันที่ไม่เกี่ยวข้องกัน เพียงแต่ว่าภาษา C++ ได้แนะนำแนวคิดเรื่องการโอเวอร์โหลดและการเขียนทับเพื่อจำลองโลกแห่งความเป็นจริงและช่วยให้โปรแกรมเมอร์สามารถจัดการกับปัญหาในโลกแห่งความเป็นจริงได้อย่างสังหรณ์ใจยิ่งขึ้น การโอเวอร์โหลดอยู่ภายใต้ขอบเขตเนมสเปซเดียวกัน ในขณะที่การแทนที่อยู่ภายใต้ขอบเขตเนมสเปซที่แตกต่างกัน ตัวอย่างเช่น คลาสพื้นฐานและคลาสที่ได้รับเป็นขอบเขตเนมสเปซที่แตกต่างกันสองขอบเขต ในระหว่างกระบวนการสืบทอด หากคลาสที่ได้รับมีชื่อเดียวกันกับฟังก์ชันคลาสพื้นฐาน ฟังก์ชันคลาสพื้นฐานจะถูกซ่อน แน่นอนว่า สถานการณ์ที่กล่าวถึงในที่นี้คือไม่มีคีย์เวิร์ดเสมือนอยู่หน้าฟังก์ชันคลาสพื้นฐาน เราจะหารือเกี่ยวกับสถานการณ์เมื่อมีคำหลักเสมือนแยกกัน
คลาสที่สืบทอดมาจะแทนที่เวอร์ชันฟังก์ชันของคลาสฐานเพื่อสร้างอินเทอร์เฟซการทำงานของตัวเอง ในเวลานี้ คอมไพเลอร์ C++ คิดว่าเนื่องจากตอนนี้คุณต้องการใช้อินเทอร์เฟซที่เขียนใหม่ของคลาสที่ได้รับ อินเทอร์เฟซของคลาสพื้นฐานของฉันจะไม่ได้รับมอบให้คุณ (แน่นอน คุณสามารถใช้วิธีการประกาศขอบเขตเนมสเปซอย่างชัดเจนได้ ดู [พื้นฐาน C++] การโอเวอร์โหลด การเขียนทับ ความหลากหลาย และการซ่อนฟังก์ชัน (1)) โดยไม่สนใจว่าอินเทอร์เฟซของคลาสพื้นฐานของคุณมีลักษณะการโอเวอร์โหลด หากคุณต้องการที่จะรักษาคุณสมบัติการโอเวอร์โหลดในคลาสที่ได้รับต่อไป ให้มอบคุณสมบัติการโอเวอร์โหลดให้กับอินเทอร์เฟซด้วยตัวคุณเอง ดังนั้นในคลาสที่ได้รับมา ตราบใดที่ชื่อฟังก์ชันเหมือนกัน เวอร์ชันฟังก์ชันของคลาสพื้นฐานจะถูกบล็อกอย่างโหดเหี้ยม ในคอมไพเลอร์ การมาสก์ถูกนำมาใช้ผ่านขอบเขตเนมสเปซ
ดังนั้น เพื่อรักษาเวอร์ชันที่โอเวอร์โหลดของฟังก์ชันคลาสพื้นฐานในคลาสที่ได้รับ คุณควรแทนที่คลาสพื้นฐานเวอร์ชันที่โอเวอร์โหลดทั้งหมด การโอเวอร์โหลดใช้ได้เฉพาะในคลาสปัจจุบันเท่านั้น และการสืบทอดจะสูญเสียคุณสมบัติของฟังก์ชันโอเวอร์โหลด กล่าวอีกนัยหนึ่ง หากคุณต้องการใส่ฟังก์ชันโอเวอร์โหลดของคลาสพื้นฐานในคลาสที่ได้รับสืบทอด คุณต้องเขียนมันใหม่
"ซ่อน" ในที่นี้หมายความว่าฟังก์ชันของคลาสที่ได้รับจะบล็อกฟังก์ชันคลาสพื้นฐานที่มีชื่อเดียวกัน ให้เราสรุปโดยย่อเกี่ยวกับกฎเฉพาะ:
ถ้าฟังก์ชันของคลาสที่ได้รับมีชื่อเดียวกันกับฟังก์ชันของคลาสพื้นฐาน แต่พารามิเตอร์ต่างกัน ในเวลานี้ ถ้าคลาสฐานไม่มีคีย์เวิร์ดเสมือน ฟังก์ชันของคลาสฐานจะถูกซ่อน (ระวังอย่าสับสนกับการโอเวอร์โหลด แม้ว่าฟังก์ชันที่มีชื่อเดียวกันและพารามิเตอร์ต่างกันควรจะเรียกว่าโอเวอร์โหลด แต่ก็ไม่สามารถเข้าใจได้ว่าเป็นโอเวอร์โหลดในที่นี้ เนื่องจากคลาสที่ได้รับและคลาสพื้นฐานไม่อยู่ในขอบเขตเนมสเปซเดียวกัน นี่คือ เข้าใจว่าปกปิด)
ถ้าฟังก์ชันของคลาสที่ได้รับมีชื่อเดียวกันกับฟังก์ชันของคลาสพื้นฐาน แต่พารามิเตอร์ต่างกัน ในเวลานี้ หากคลาสฐานมีคีย์เวิร์ดเสมือน ฟังก์ชันของคลาสฐานจะถูกสืบทอดโดยปริยายไปยัง vtable ของคลาสที่ได้รับ ในขณะนี้ ฟังก์ชันในคลาสที่ได้รับ vtable ชี้ไปยังที่อยู่ฟังก์ชันของเวอร์ชันคลาสพื้นฐาน ในเวลาเดียวกัน เวอร์ชันฟังก์ชันใหม่นี้จะถูกเพิ่มให้กับคลาสที่ได้รับเป็นเวอร์ชันที่โอเวอร์โหลดของคลาสที่ได้รับ แต่เมื่อตัวชี้คลาสพื้นฐานใช้วิธีการเรียกฟังก์ชันแบบ polymorphic เวอร์ชันฟังก์ชันคลาสที่ได้รับใหม่นี้จะถูกซ่อนไว้
หากฟังก์ชันของคลาสที่ได้รับมีชื่อเดียวกันกับฟังก์ชันของคลาสฐานและพารามิเตอร์ก็เหมือนกัน แต่ฟังก์ชันคลาสฐานไม่มีคีย์เวิร์ดเสมือน ในเวลานี้ ฟังก์ชันของคลาสพื้นฐานถูกซ่อนอยู่ (ระวังอย่าสับสนกับความคุ้มครองซึ่งเข้าใจกันว่าซ่อนอยู่)
หากฟังก์ชันของคลาสที่ได้รับมีชื่อเดียวกันกับฟังก์ชันของคลาสฐานและพารามิเตอร์ก็เหมือนกัน แต่ฟังก์ชันคลาสฐานมีคีย์เวิร์ดเสมือน ในเวลานี้ ฟังก์ชันของคลาสพื้นฐานจะไม่ถูก "ซ่อน" (ตรงนี้ต้องทำความเข้าใจเป็นหลักนะครับ ^_^)
สลับฉาก: เมื่อไม่มีคีย์เวิร์ดเสมือนอยู่หน้าฟังก์ชันคลาสพื้นฐาน เราจำเป็นต้องเขียนมันใหม่ให้ราบรื่นยิ่งขึ้น เมื่อมีคีย์เวิร์ดเสมือน มันจะสมเหตุสมผลกว่าที่จะเรียกมันว่าแทนที่ ทุกคนสามารถเข้าใจ C++ ได้ดีขึ้น มีบางสิ่งที่ละเอียดอ่อน เพื่อเป็นการไม่ให้เสียเวลา เรามาอธิบายด้วยตัวอย่างกันดีกว่า
ตัวอย่างที่ 6
#include <iostream>โดยใช้ namespace std; คลาส Base{public: virtual void fun() { cout << "Base::fun()" << endl; }//overload virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload}; มาจาก: public Base{public: void fun() { cout << "ได้รับ::fun()" << endl; }//แทนที่ void fun(int i) { cout << "Derive::fun(int i)" << endl; }//แทนที่ 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' : ไม่มีฟังก์ชันที่โอเวอร์โหลดต้องใช้พารามิเตอร์ 2 ตัว cout << endl; pd = สืบทอดใหม่ (); pd-> สนุก (1); pd-> สนุก (1,2); // ลบเกิน pb;
ผลลัพธ์เอาท์พุต
ได้มา::สนุก()
สืบทอด::fun(int i)
ได้มา::สนุก()
สืบทอด::fun(int i)
สืบทอดมา::สนุก(int i,int j)
กดปุ่มใดก็ได้เพื่อดำเนินการต่อ
-
ตัวอย่างที่ 7-1
#include <iostream> โดยใช้ namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) ลบ pb;
ตัวอย่างที่ 7-2
#include <iostream> โดยใช้ namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; เป็นโมฆะสนุก (double d) { cout << "Derive :: fun (double d)" << endl; int main () { Base *pb = new Derive (); pb->สนุก(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) ลบ pb;
ตัวอย่างที่ 8-1
#include <iostream> โดยใช้ namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; เป็นโมฆะความสนุก (int i) { cout << "Derive :: fun (int i)" << endl; int main () { Base *pb = new Derive (); pb->fun(1);//Derive::fun(int i) ลบ pb;
ตัวอย่างที่ 8-2
#include <iostream> โดยใช้ namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; เป็นโมฆะสนุก (int i) { cout << "ได้มา :: สนุก (int i)" << endl; } เป็นโมฆะสนุก (double d) { cout << "ได้มา :: สนุก (double d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) pb->fun((double) 0.01);//รับมา::สนุก(int i) ลบ pb; กลับ 0;}
ตัวอย่างที่ 9
#include <iostream> โดยใช้ namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; เป็นโมฆะสนุก (int i) { cout << "ได้รับ :: สนุก (int i)" << endl; } สนุกเป็นโมฆะ (ถ่าน c) { cout << "ได้รับ:: สนุก (ถ่าน c)" << endl; } ความสนุกเป็นโมฆะ(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(); pd->fun(1);//Derive::fun(int i) // โอเวอร์โหลด pd->สนุก ('a'); // ได้มา :: สนุก (ถ่าน c) // โอเวอร์โหลด pd-> สนุก (0.01); // ได้มา :: สนุก (ดับเบิล d) ลบ pb; ลบ pd;
ตัวอย่างที่ 7-1 และ 8-1 นั้นง่ายต่อการเข้าใจ ฉันจึงยกตัวอย่างทั้งสองนี้ไว้ที่นี่เพื่อให้ทุกคนได้เปรียบเทียบและช่วยให้ทุกคนเข้าใจได้ดีขึ้น:
ในตัวอย่างที่ 7-1 คลาสที่ได้รับไม่ครอบคลุมฟังก์ชันเสมือนของคลาสฐาน ในขณะนี้ ที่อยู่ที่ชี้โดยตัวชี้ฟังก์ชันใน vtable ของคลาสที่ได้รับคือแอดเดรสฟังก์ชันเสมือนของคลาสฐาน
ในตัวอย่างที่ 8-1 คลาสที่ได้รับจะแทนที่ฟังก์ชันเสมือนของคลาสพื้นฐาน ในขณะนี้ ที่อยู่ที่ชี้ไปโดยตัวชี้ฟังก์ชันใน vtable ของคลาสที่ได้รับคือที่อยู่ของฟังก์ชันเสมือนที่ถูกแทนที่ของคลาสที่ได้รับ
ตัวอย่างที่ 7-2 และ 8-2 ดูแปลกไปสักหน่อย จริงๆ แล้วถ้าเปรียบเทียบตามหลักการข้างต้น คำตอบก็จะชัดเจน:
ในตัวอย่างที่ 7-2 เราโอเวอร์โหลดฟังก์ชันเวอร์ชันสำหรับคลาสที่ได้รับ: void fun(double d) อันที่จริง นี่เป็นเพียงการปกปิดเท่านั้น มาวิเคราะห์กันโดยเฉพาะ มีหลายฟังก์ชันในคลาสพื้นฐานและหลายฟังก์ชันในคลาสที่ได้รับ:
ประเภทคลาสพื้นฐานคลาสที่ได้รับ
ส่วนวีเทเบิล
ความสนุกเป็นโมฆะ (int i)
ชี้ไปที่เวอร์ชันคลาสพื้นฐานของฟังก์ชันเสมือน void fun(int i)
ส่วนคงที่
ความสนุกเป็นโมฆะ(d สองเท่า)
มาวิเคราะห์โค้ดสามบรรทัดต่อไปนี้อีกครั้ง:
ฐาน *pb = สืบทอดใหม่();
pb->สนุก(1);//ฐาน::สนุก(int i)
pb->สนุก((สองเท่า)0.01);//Base::fun(int i)
ประโยคแรกคือกุญแจสำคัญ ตัวชี้คลาสพื้นฐานชี้ไปที่วัตถุของคลาสที่ได้รับ เรารู้ว่านี่คือการเรียกแบบหลายสัณฐาน ประเภทของวัตถุรันไทม์ ดังนั้นก่อนอื่นให้ไปที่ vtable ของคลาสที่ได้รับเพื่อค้นหาเวอร์ชันฟังก์ชันเสมือนของคลาสที่ได้รับ พบว่าคลาสที่ได้รับไม่ครอบคลุมฟังก์ชันเสมือนของคลาสฐาน คลาสที่ได้รับจะสร้างตัวชี้ไปยังที่อยู่ของฟังก์ชันเสมือนของคลาสพื้นฐานเท่านั้น ดังนั้นจึงเป็นเรื่องปกติที่จะเรียกคลาสพื้นฐานเวอร์ชันคลาสของฟังก์ชันเสมือน ในประโยคสุดท้าย โปรแกรมยังคงมองหา vtable ของคลาสที่ได้รับ และพบว่าไม่มีฟังก์ชันเสมือนของเวอร์ชันนี้เลย จึงต้องย้อนกลับไปเรียกใช้ฟังก์ชันเสมือนของตัวเอง
นอกจากนี้ยังควรกล่าวถึง ณ ที่นี้ด้วยว่าหากคลาสพื้นฐานมีฟังก์ชันเสมือนหลายฟังก์ชันในเวลานี้ โปรแกรมจะแจ้ง "การเรียกที่ไม่ชัดเจน" เมื่อคอมไพล์โปรแกรม ตัวอย่างมีดังนี้
#include <iostream> โดยใช้ namespace 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; }}; d)"<< endl; } }; int main(){ Base *pb = new Derive(); pb->fun(0.01);//error C2668: 'fun' : การเรียกที่ไม่ชัดเจนไปยังฟังก์ชันที่โอเวอร์โหลด ลบ pb; return 0;}
เอาล่ะ เรามาวิเคราะห์ตัวอย่างที่ 8-2 กันอีกครั้ง
ในตัวอย่างที่ 8-2 เรายังโอเวอร์โหลดเวอร์ชันฟังก์ชันสำหรับคลาสที่ได้รับ: void fun(double d) และยังครอบคลุมฟังก์ชันเสมือนของคลาสพื้นฐานด้วย มาวิเคราะห์กันโดยละเอียด มีฟังก์ชันหลายอย่างในคลาสพื้นฐาน และคลาสที่ได้รับมีหลายฟังก์ชัน คลาสมีหลายฟังก์ชัน:
ประเภทคลาสพื้นฐานคลาสที่ได้รับ
ส่วนวีเทเบิล
ความสนุกเป็นโมฆะ (int i)
ความสนุกเป็นโมฆะ (int i)
ส่วนคงที่
ความสนุกเป็นโมฆะ(d สองเท่า)
จากตาราง เราจะเห็นว่าตัวชี้ฟังก์ชันใน vtable ของคลาสที่ได้รับนั้นชี้ไปยังที่อยู่ฟังก์ชันเสมือนที่ถูกแทนที่ของตัวเอง
มาวิเคราะห์โค้ดสามบรรทัดต่อไปนี้อีกครั้ง:
ฐาน *pb = สืบทอดใหม่();
pb->สนุก(1);//ได้มา::สนุก(int i)
pb->fun((double)0.01);//Derive::fun(int i)
ไม่จำเป็นต้องพูดอะไรเพิ่มเติมเกี่ยวกับประโยคแรก ประโยคที่สองคือการเรียกเวอร์ชันฟังก์ชันเสมือนของคลาสที่ได้รับ แน่นอนว่า ประโยคที่สาม เฮ้ มันรู้สึกแปลกๆ อีกแล้ว จริงๆ แล้วโปรแกรม C++ นั้นแย่มาก โง่ เมื่อทำงานพวกเขาก็จมอยู่ในตาราง vtable ของคลาสที่ได้รับฉันแค่ดูมันและรู้ว่าไม่มีเวอร์ชันที่ฉันต้องการฉันไม่สามารถเข้าใจได้จริงๆ ทำไมตัวชี้คลาสพื้นฐานไม่มองไปรอบ ๆ และมองหามัน ฮ่าฮ่า ปรากฎว่าสายตามีจำกัด มันเก่ามาก ดังนั้นมันจึงต้องมีสายตายาวตามอายุ ส่วน Vtable (นั่นคือส่วนคงที่) และส่วน Vtable ที่คุณต้องการจัดการ ถือเป็นโมฆะของคลาสที่ได้รับ ความสนุก(double d) อยู่ไกลมาก มองไม่เห็นเลย! นอกจากนี้ คลาสที่ได้รับมายังต้องดูแลทุกอย่างอีกด้วย คลาสที่ได้รับมานั้นมีพลังของตัวเองไม่ใช่เหรอ เฮ้ ไม่ต้องเถียงกันอีกแล้ว ทุกคนทำได้ ดูแลตัวเอง^_^
อนิจจา! คุณจะถอนหายใจไหม ตัวชี้คลาสพื้นฐานสามารถโทรแบบ polymorphic ได้ แต่ไม่สามารถโทรไปยังคลาสที่ได้รับมากเกินไปได้ (อ้างอิงถึงตัวอย่างที่ 6)~~~
ลองดูตัวอย่างที่ 9 อีกครั้ง
ผลกระทบของตัวอย่างนี้เหมือนกับตัวอย่างที่ 6 โดยมีจุดประสงค์เดียวกัน ฉันเชื่อว่าหลังจากที่คุณเข้าใจตัวอย่างข้างต้นแล้ว นี่ก็เป็นจูบเล็กๆ น้อยๆ เช่นกัน
สรุป:
การโอเวอร์โหลดจะเลือกเวอร์ชันของฟังก์ชันที่จะเรียกตามรายการพารามิเตอร์ของฟังก์ชัน ในขณะที่ polymorphism เลือกเวอร์ชันของฟังก์ชันเสมือนที่จะเรียกตามประเภทจริงของออบเจ็กต์รันไทม์ Polymorphism ถูกนำมาใช้ผ่านคลาสที่ได้รับมาจนถึงคลาสพื้นฐานเสมือน ฟังก์ชันถูกนำไปใช้โดยการแทนที่ หากคลาสที่ได้รับไม่แทนที่ฟังก์ชันเสมือนเสมือนของคลาสพื้นฐาน คลาสที่ได้รับจะสืบทอดฟังก์ชันเสมือนเสมือนของคลาสฐานโดยอัตโนมัติ เวอร์ชันของฟังก์ชัน ในขณะนี้ ไม่ว่าวัตถุที่ชี้โดยตัวชี้คลาสพื้นฐานจะเป็นชนิดพื้นฐานหรือชนิดที่ได้รับมา ฟังก์ชันเสมือนของเวอร์ชันคลาสพื้นฐานจะถูกเรียก ถ้าคลาสที่ได้รับมาจะแทนที่ฟังก์ชันเสมือนเสมือน ของคลาสฐานนั้นจะถูกเรียกขณะรันไทม์ตามประเภทที่แท้จริงของวัตถุที่ใช้เพื่อเลือกเวอร์ชันฟังก์ชันเสมือนเสมือนที่จะเรียกใช้ ตัวอย่างเช่น หากประเภทวัตถุที่ชี้โดยตัวชี้คลาสฐานเป็นประเภทที่ได้รับ เวอร์ชันฟังก์ชันเสมือนเสมือนของคลาสที่ได้รับจะถูกเรียก ดังนั้นจึงทำให้เกิดความหลากหลาย
ความตั้งใจเดิมของการใช้ความหลากหลายคือการประกาศฟังก์ชันเสมือนในคลาสพื้นฐาน และเพื่อแทนที่เวอร์ชันฟังก์ชันเสมือนเสมือนของคลาสพื้นฐานในคลาสที่ได้รับ โปรดทราบว่าฟังก์ชันต้นแบบในขณะนี้สอดคล้องกับคลาสฐาน นั่นคือชื่อเดียวกันและชื่อเดียวกัน หากคุณเพิ่มเวอร์ชันฟังก์ชันใหม่ให้กับคลาสที่ได้รับ คุณจะไม่สามารถเรียกเวอร์ชันฟังก์ชันใหม่ของคลาสที่ได้รับแบบไดนามิกผ่านตัวชี้คลาสฐานเท่านั้น เป็นเวอร์ชันที่โอเวอร์โหลดของคลาสที่ได้รับ ประโยคเดียวกันนี้ การโอเวอร์โหลดใช้ได้เฉพาะในคลาสปัจจุบัน ไม่ว่าคุณจะโอเวอร์โหลดในคลาสพื้นฐานหรือคลาสที่ได้รับ ทั้งสองจะไม่เกี่ยวข้องกัน หากเราเข้าใจสิ่งนี้ เราก็สามารถเข้าใจผลลัพธ์ที่ได้ในตัวอย่างที่ 6 และ 9 ได้สำเร็จเช่นกัน
การโอเวอร์โหลดถูกผูกไว้แบบคงที่ ส่วนความหลากหลายถูกผูกไว้แบบไดนามิก เพื่ออธิบายเพิ่มเติม การโอเวอร์โหลดไม่เกี่ยวข้องกับประเภทของวัตถุที่ตัวชี้ชี้ไปจริงๆ และความหลากหลายนั้นสัมพันธ์กับประเภทของวัตถุที่ตัวชี้ชี้ไปจริงๆ ถ้าตัวชี้คลาสพื้นฐานเรียกคลาสที่ได้รับมาในเวอร์ชันที่โอเวอร์โหลด คอมไพเลอร์ C++ จะถือว่ามันผิดกฎหมาย คอมไพเลอร์ C++ คิดว่าตัวชี้คลาสพื้นฐานสามารถเรียกคลาสพื้นฐานเวอร์ชันที่โอเวอร์โหลดเท่านั้น และการโอเวอร์โหลดใช้งานได้ในเนมสเปซเท่านั้น ของคลาสปัจจุบัน ถูกต้องภายในโดเมน การสืบทอดจะสูญเสียคุณสมบัติการโอเวอร์โหลด แน่นอนว่าหากตัวชี้คลาสพื้นฐานในเวลานี้เรียกใช้ฟังก์ชันเสมือน จากนั้นจะเลือกเวอร์ชันฟังก์ชันเสมือนเสมือนของคลาสพื้นฐานหรือเวอร์ชันฟังก์ชันเสมือนเสมือนของคลาสที่ได้รับเพื่อดำเนินการเฉพาะ ซึ่งจะถูกกำหนดโดยประเภทของอ็อบเจ็กต์ที่ชี้โดยตัวชี้คลาสฐาน ดังนั้นการโอเวอร์โหลดและพอยน์เตอร์ ประเภทของวัตถุที่ตัวชี้ชี้ไปนั้นไม่เกี่ยวอะไรกับวัตถุนั้น ความหลากหลายนั้นสัมพันธ์กับประเภทของวัตถุที่ตัวชี้ชี้ไปจริงๆ
สุดท้ายนี้ เพื่อชี้แจงให้ชัดเจนว่าฟังก์ชันเสมือนเสมือนสามารถโอเวอร์โหลดได้ แต่การโอเวอร์โหลดจะมีผลภายในขอบเขตของเนมสเปซปัจจุบันจำนวนเท่าใดเท่านั้น
ก่อนอื่นเรามาดูโค้ดกันก่อน:
รหัสจาวา
สตริง str=สตริงใหม่("abc");
โค้ดนี้มักจะตามมาด้วยคำถาม นั่นคือ โค้ดบรรทัดนี้สร้างออบเจ็กต์สตริงจำนวนเท่าใด ฉันเชื่อว่าทุกคนคุ้นเคยกับคำถามนี้และคำตอบก็รู้กันดี 2. ต่อไป เราจะเริ่มจากคำถามนี้และทบทวนความรู้ JAVA บางส่วนที่เกี่ยวข้องกับการสร้างวัตถุ String
เราสามารถแบ่งบรรทัดโค้ดด้านบนออกเป็นสี่ส่วน: String str, =, "abc" และ new String() String str กำหนดเฉพาะตัวแปรประเภท String ที่ชื่อ str ดังนั้นจึงไม่สร้างวัตถุ = เตรียมใช้งานตัวแปร str และกำหนดการอ้างอิง (หรือตัวจัดการ) ให้กับวัตถุนั้น และเห็นได้ชัดว่าไม่ได้สร้างวัตถุ ; ตอนนี้เป็นเพียงสิ่งใหม่เท่านั้น เหลือสตริง ("abc") เหตุใด new String("abc") จึงถือเป็น "abc" และ new String() ได้ ลองมาดูที่ตัวสร้าง String ที่เราเรียกว่า:
รหัสจาวา
สตริงสาธารณะ (สตริงดั้งเดิม) {
//โค้ดอื่นๆ...
-
ดังที่เราทุกคนทราบกันดีว่ามีสองวิธีที่ใช้กันทั่วไปในการสร้างอินสแตนซ์ (อ็อบเจ็กต์) ของคลาส:
ใช้ใหม่เพื่อสร้างวัตถุ
เรียกเมธอด newInstance ของคลาส Class และใช้กลไกการสะท้อนเพื่อสร้างวัตถุ
เราใช้ new เพื่อเรียกเมธอด Constructor ข้างต้นของคลาส String เพื่อสร้างอ็อบเจ็กต์และกำหนดการอ้างอิงให้กับตัวแปร str ในเวลาเดียวกัน เราสังเกตเห็นว่าพารามิเตอร์ที่ยอมรับโดยเมธอดคอนสตรัคเตอร์ที่เรียกว่านั้นเป็นอ็อบเจ็กต์ String เช่นกัน และอ็อบเจ็กต์นี้คือ "abc" พอดี จากนี้เราจะต้องแนะนำวิธีอื่นในการสร้างวัตถุ String - ข้อความที่อยู่ภายในเครื่องหมายคำพูด
วิธีนี้เป็นลักษณะเฉพาะของ String และแตกต่างจากวิธีใหม่อย่างมาก
รหัสจาวา
สตริง str="abc";
ไม่ต้องสงสัยเลยว่าบรรทัดโค้ดนี้สร้างวัตถุ String
รหัสจาวา
สตริง a="abc";
สตริง b="abc";
แล้วที่นี่ล่ะ? คำตอบยังคงเป็นหนึ่ง
รหัสจาวา
สตริง a="ab"+"cd";
แล้วที่นี่ล่ะ? คำตอบยังคงเป็นหนึ่ง แปลกนิดหน่อย? ณ จุดนี้ เราจำเป็นต้องแนะนำการทบทวนความรู้ที่เกี่ยวข้องกับ String Pool
มีพูลสตริงใน JAVA Virtual Machine (JVM) ซึ่งจัดเก็บอ็อบเจ็กต์ String จำนวนมากและสามารถแชร์ได้ ดังนั้นจึงปรับปรุงประสิทธิภาพ เนื่องจากคลาส String ถือเป็นคลาสสุดท้าย เมื่อสร้างแล้วค่าจะไม่สามารถเปลี่ยนแปลงได้ ดังนั้นเราจึงไม่ต้องกังวลเกี่ยวกับความสับสนของโปรแกรมที่เกิดจากการแชร์อ็อบเจ็กต์ String สตริงพูลได้รับการดูแลโดยคลาส String และเราสามารถเรียกใช้เมธอด intern() เพื่อเข้าถึงสตริงพูลได้
ลองย้อนกลับไปดูที่ String a="abc"; ค่าที่ส่งคืนของคลาส String เท่ากับ (Object obj) วิธีการ ถ้ามี จะไม่มีการสร้างออบเจ็กต์ใหม่ และการอ้างอิงไปยังออบเจ็กต์ที่มีอยู่จะถูกส่งกลับโดยตรง หากไม่มี ออบเจ็กต์จะถูกสร้างขึ้นก่อน จากนั้นจึงเพิ่มลงในพูลสตริง จากนั้นการอ้างอิงจะถูกส่งคืน ดังนั้นจึงไม่ยากที่จะเข้าใจว่าทำไมสองตัวอย่างแรกจากสามตัวอย่างก่อนหน้าจึงมีคำตอบนี้
สำหรับตัวอย่างที่สาม:
รหัสจาวา
สตริง a="ab"+"cd";
เพราะค่าของค่าคงที่จะถูกกำหนด ณ เวลาคอมไพล์ ในที่นี้ "ab" และ "cd" เป็นค่าคงที่ ดังนั้นจึงสามารถกำหนดค่าของตัวแปร a ได้ในขณะคอมไพล์ ผลการคอมไพล์ของโค้ดบรรทัดนี้เทียบเท่ากับ:
รหัสจาวา
สตริง a="abcd";
ดังนั้นจึงมีการสร้างออบเจ็กต์ "abcd" เพียงรายการเดียวเท่านั้น และจะถูกบันทึกไว้ในพูลสตริง
ตอนนี้คำถามกลับมาอีกครั้ง สตริงทั้งหมดที่ได้รับหลังจากการเชื่อมต่อ "+" จะถูกเพิ่มลงในกลุ่มสตริงหรือไม่ เราทุกคนรู้ดีว่า "==" สามารถใช้เปรียบเทียบตัวแปรสองตัวได้ โดยมี 2 สถานการณ์ดังต่อไปนี้:
หากมีการเปรียบเทียบสองประเภทพื้นฐาน (char, byte, short, int, long, float, double, boolean) ระบบจะตัดสินว่าค่าของพวกเขาเท่ากันหรือไม่
หากตารางเปรียบเทียบตัวแปรออบเจ็กต์สองตัว ระบบจะตัดสินว่าการอ้างอิงตัวแปรเหล่านั้นชี้ไปที่ออบเจ็กต์เดียวกันหรือไม่
ต่อไปเราจะใช้ "==" เพื่อทำการทดสอบบางอย่าง เพื่อความสะดวกในการอธิบาย เราถือว่าการชี้ไปที่ออบเจ็กต์ที่มีอยู่แล้วในสตริงพูลเป็นออบเจ็กต์ที่ถูกเพิ่มลงในพูลสตริง:
รหัสจาวา
StringTest ระดับสาธารณะ { โมฆะสาธารณะคงที่ 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 ได้ถูกเพิ่มลงในพูลสตริงด้วยถ้า (d == c ) { System.out.println("/"ab/"+/"cd/" The create object/" is added to/" the string pool"); } // ถ้า d และ c ไม่ได้ชี้ไปที่เดียวกัน วัตถุหมายความว่า d ไม่ได้ถูกเพิ่มลงในพูลสตริง else { System.out.println("/"ab/"+/"cd/" The create object/"is not added/" to the string pool"); } สตริง e = a + "cd"; c ชี้ไปที่วัตถุเดียวกัน หมายความว่า e ถูกเพิ่มลงในพูลสตริงด้วย ถ้า (e == c) { System.out.println(" a +/"cd/" The create object/"joined/" string สระกลาง"); } // หาก e และ c ไม่ได้ชี้ไปที่วัตถุเดียวกัน หมายความว่า e ไม่ได้ถูกเพิ่มลงในพูลสตริง else { System.out.println(" a +/"cd/" create object/"not added/" ไปที่ string pool" ); } String f = "ab" + b; // ถ้า f และ c ชี้ไปที่วัตถุเดียวกัน หมายความว่า f ถูกเพิ่มเข้าไปใน string pool ด้วย ถ้า (f == c) { System.out .println("/ วัตถุที่สร้างโดย "ab/"+ b /"Added/" to the string pool"); } // ถ้า f และ c ไม่ได้ชี้ไปที่วัตถุเดียวกัน หมายความว่า f ไม่ได้ถูกเพิ่มลงใน string pool else { System.out.println("/" ab/" + b วัตถุที่สร้าง/"not added/" to the string pool"); } String g = a + b; // ถ้า g และ c ชี้ไปที่วัตถุเดียวกัน แสดงว่า g ถูกเพิ่มเข้าไปใน สตริงพูล if ( g == c) { System.out.println(" a + b The create object/"added/" to the string pool"); } // ถ้า g และ c ไม่ได้ชี้ไปที่วัตถุเดียวกัน หมายความว่า g ไม่ได้ถูกเพิ่มลงใน string pool else { System.out.println (" a + b วัตถุที่สร้างขึ้น/"ไม่ได้เพิ่ม/" ลงในพูลสตริง");
ผลการวิ่งมีดังนี้:
สตริง a = "ab";
สตริง b = "ซีดี";
วัตถุที่สร้างโดย "ab"+"cd" คือ "เข้าร่วม" ในกลุ่มสตริง
วัตถุที่สร้างโดย + "cd" คือ "ไม่ได้เพิ่ม" ลงในพูลสตริง
วัตถุที่สร้างโดย "ab"+ b คือ "ไม่ได้เพิ่ม" ลงในพูลสตริง
วัตถุที่สร้างโดย a + b คือ "ไม่ได้เพิ่ม" ลงในพูลสตริง จากผลลัพธ์ข้างต้น เราจะเห็นได้อย่างง่ายดายว่าจะมีการเพิ่มเฉพาะวัตถุใหม่ที่สร้างขึ้นโดยใช้การเชื่อมต่อ "+" ระหว่างวัตถุ String ที่สร้างขึ้นโดยใช้เครื่องหมายคำพูดเพื่อรวมข้อความ . ในสระสตริง สำหรับนิพจน์การเชื่อมต่อ "+" ทั้งหมดที่มีออบเจ็กต์ที่สร้างขึ้นในโหมดใหม่ (รวมถึงค่าว่าง) ออบเจ็กต์ใหม่ที่สร้างขึ้นจะไม่ถูกเพิ่มลงในพูลสตริง และเราจะไม่ลงรายละเอียดเกี่ยวกับเรื่องนี้
แต่มีสถานการณ์หนึ่งที่เรียกร้องความสนใจจากเรา โปรดดูรหัสด้านล่าง:
รหัสจาวา
StringStaticTest คลาสสาธารณะ { // ค่าคงที่สาธารณะคงสุดท้าย String A = "ab"; // ค่าคงที่สาธารณะ B สุดท้ายคงสตริง B = "cd"; โมฆะสาธารณะคงหลัก (สตริง [] args) { // ใช้สองค่าคงที่ + เชื่อมต่อ เพื่อเริ่มต้น s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s เท่ากับ t พวกเขาเป็นวัตถุเดียวกัน" }); { System.out.println("s ไม่เท่ากับ t ไม่ใช่วัตถุเดียวกัน");
ผลลัพธ์ของการรันโค้ดนี้จะเป็นดังนี้:
s เท่ากับ t พวกมันคือวัตถุเดียวกัน เพราะเหตุใด? เหตุผลก็คือ สำหรับค่าคงที่ ค่าของมันได้รับการแก้ไข จึงสามารถกำหนดได้ ณ เวลาคอมไพล์ ในขณะที่ค่าของตัวแปรสามารถระบุได้เฉพาะที่รันไทม์เท่านั้น เนื่องจากตัวแปรนี้สามารถเรียกได้ด้วยวิธีการที่แตกต่างกัน ดังนั้นอาจทำให้เกิด มูลค่าที่จะเปลี่ยนแปลง ในตัวอย่างข้างต้น A และ B เป็นค่าคงที่และค่าของพวกมันได้รับการแก้ไข ดังนั้นค่าของ s จึงได้รับการแก้ไขเช่นกัน และจะถูกกำหนดเมื่อมีการคอมไพล์คลาส กล่าวคือ:
รหัสจาวา
สตริง s=A+B;
เทียบเท่ากับ:
รหัสจาวา
สตริง s="ab"+"cd";
ฉันขอเปลี่ยนตัวอย่างข้างต้นเล็กน้อยแล้วดูว่าเกิดอะไรขึ้น:
รหัสจาวา
คลาสสาธารณะ StringStaticTest { // ค่าคงที่สาธารณะสตริงสุดท้ายคงที่ (สตริง [] args) { // เริ่มต้น 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 จึงมีพฤติกรรมเหมือนตัวแปรก่อนที่จะกำหนดค่า ดังนั้นไม่สามารถระบุได้ในเวลาคอมไพล์ แต่สามารถสร้างได้เฉพาะตอนรันไทม์เท่านั้น
เนื่องจากการแชร์ออบเจ็กต์ใน String Pool สามารถปรับปรุงประสิทธิภาพได้ เราจึงสนับสนุนให้ทุกคนสร้างออบเจ็กต์ String โดยรวมข้อความไว้ในเครื่องหมายคำพูด จริงๆ แล้วนี่คือสิ่งที่เรามักใช้ในการเขียนโปรแกรม
ต่อไปเรามาดูวิธีการฝึกงาน () ซึ่งมีการกำหนดไว้ดังนี้:
รหัสจาวา
สตริงฝึกงานสาธารณะ ();
นี่เป็นวิธีดั้งเดิม เมื่อเรียกใช้เมธอดนี้ เครื่องเสมือน JAVA จะตรวจสอบว่าวัตถุมีค่าเท่ากับวัตถุที่มีอยู่แล้วในกลุ่มสตริงหรือไม่ ถ้าเป็นเช่นนั้น จะส่งกลับการอ้างอิงไปยังวัตถุในพูลสตริง ถ้าไม่มี จะสร้างการ วัตถุในพูลสตริง วัตถุสตริงที่มีค่าเดียวกันแล้วส่งคืนการอ้างอิง
ลองดูรหัสนี้:
รหัสจาวา
public class StringInternTest { public static void main(String[] args) { // ใช้อาร์เรย์ถ่านเพื่อเริ่มต้น a เพื่อหลีกเลี่ยงไม่ให้วัตถุที่มีค่า "abcd" มีอยู่แล้วในกลุ่มสตริงก่อนที่จะสร้าง a String a = new String ( ถ่านใหม่[] { 'a', 'b', 'c', 'd' }); สตริง b = a.intern(); System.out.println("b ถูกเพิ่มลงใน string pool, ไม่มีการสร้าง object ใหม่"); } else { System.out.println("b ไม่ได้ถูกเพิ่มลงใน string pool, ไม่มีการสร้าง object ใหม่"); } } }
ผลการวิ่ง:
b ไม่ได้ถูกเพิ่มลงในพูลสตริงและมีการสร้างอ็อบเจ็กต์ใหม่ หากเมธอด intern() ของคลาส String ไม่พบอ็อบเจ็กต์ที่มีค่าเดียวกัน มันจะเพิ่มอ็อบเจ็กต์ปัจจุบันลงในพูลสตริงแล้วส่งคืน การอ้างอิง จากนั้น b และ a ชี้ไปที่วัตถุเดียวกัน มิฉะนั้น วัตถุที่ชี้ไปโดย b จะถูกสร้างขึ้นใหม่โดยเครื่องเสมือน JAVA ในพูลสตริง แต่ค่าของมันจะเหมือนกับ a ผลการทำงานของโค้ดด้านบนเป็นเพียงการยืนยันประเด็นนี้
สุดท้ายนี้ เรามาพูดถึงการจัดเก็บออบเจ็กต์ String ใน JAVA Virtual Machine (JVM) และความสัมพันธ์ระหว่าง string pool กับฮีปและสแต็ก ก่อนอื่นมาทบทวนความแตกต่างระหว่างฮีปและสแต็ค:
สแต็ก: บันทึกประเภทพื้นฐาน (หรือประเภทในตัว) เป็นหลัก (ถ่าน, ไบต์, สั้น, int, ยาว, ลอย, สองเท่า, บูลีน) และการอ้างอิงวัตถุสามารถแชร์ได้ และความเร็วของมันนั้นเร็วกว่าเท่านั้น กอง.
ฮีป: ใช้เพื่อจัดเก็บวัตถุ
เมื่อเราดูซอร์สโค้ดของคลาส String เราจะพบว่ามีแอตทริบิวต์ value ซึ่งเก็บค่าของวัตถุ String ประเภทคือ char[] ซึ่งแสดงว่าสตริงเป็นลำดับของอักขระด้วย
เมื่อดำเนินการ String a="abc"; เครื่องเสมือน JAVA จะสร้างค่าถ่านสามค่า 'a', 'b' และ 'c' ในสแต็ก จากนั้นสร้างวัตถุ String ในฮีป ค่าของมัน (value ) คืออาร์เรย์ของค่าถ่านสามค่าที่เพิ่งสร้างบนสแต็ก {'a', 'b', 'c'} ในที่สุด ออบเจ็กต์ String ที่สร้างขึ้นใหม่จะถูกเพิ่มลงในพูลสตริง หากเรารันโค้ด String b=new String("abc"); เนื่องจาก "abc" ถูกสร้างขึ้นและบันทึกในพูลสตริง เครื่องเสมือน JAVA จะสร้างเฉพาะออบเจ็กต์ String ใหม่ในฮีป แต่ value คือค่าประเภทถ่านสามค่า 'a', 'b' และ 'c' ที่สร้างขึ้นบนสแต็กเมื่อมีการดำเนินการโค้ดบรรทัดก่อนหน้า
ณ จุดนี้ เราค่อนข้างชัดเจนแล้วเกี่ยวกับคำถามที่ว่าทำไม String str=new String("abc") ที่ถูกยกขึ้นตอนต้นของบทความนี้จึงสร้างอ็อบเจ็กต์สองตัวขึ้นมา