Bài giảng Lập trình hướng đối tượng - Đa hình Hàm ảo

Đa hình là gì?

Đa hình hàm – function polymorphism

Đa hình và Hướng đối tượng

So sánh Overloading và Overriding

Method Overidding

Đa hình tĩnh – static polymorphism là gì?

Liên kết động – dynamic binding

Đa hình động – dynamic polymorphism

Hàm ảo – virtual function

Destructor ảo

 

ppt40 trang | Chuyên mục: Lập Trình Hướng Đối Tượng | Chia sẻ: dkS00TYs | Lượt xem: 4164 | Lượt tải: 4download
Tóm tắt nội dung Bài giảng Lập trình hướng đối tượng - Đa hình Hàm ảo, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
dCow Như vậy, nếu tạo một đối tượng Cow, và gọi hành vi makeASound() của nó, nó sẽ kêu “moo” Cow bob(“Bob”, “brown”); bob.makeASound(); Ta còn có thể tạo một con trỏ tới đối tượng Cow, và làm cho nó kêu “moo” p_bob = &bob; p_bob->makeASound(); bob *p_bob Bò điên - MadCow Bò điên (MadCow) cũng là bò Nhưng chúng tưởng mình là ếch, và kêu “oap” Bò điên - MadCow Nhưng bò điên khá là rắc rối Nếu coi nó là bò điên (MadCow), nó sẽ hành động đúng là bò điên MadCow maddy(“Maddy”, “white”); maddy.makeASound(); 	// Go “oap” MadCow * maddy = new MadCow(“Maddy”, “white”); maddy->makeASound(); 	// Go “oap” Còn khi tưởng nó chỉ là bò thường (Cow), nó lại có vẻ bình thường MadCow* maddy = new madCow(“Maddy”, “white”); Cow* cow = maddy; 	// Upcasting to a cow cow->makeASound(); 	// Go “moo” ??? Làm thế nào để bò điên lúc nào cũng điên (kêu “oap”)? Function call binding(Liên kết lời gọi hàm) Trước khi có thể làm cho bò điên lúc nào cũng điên, ta cần hiểu vấn đề rõ hơn C++ làm thế nào để tìm đúng hàm cần chạy mỗi khi ta có một lời gọi hàm hoặc gọi phương thức? Function call binding là quy trình xác định khối mã hàm cần chạy khi một lời gọi hàm được thực hiện Xem xét function call binding C, C++ method call binding static binding dynamic binding Function call binding Trong C, việc này rất dễ dàng vì mỗi tên hàm chỉ có thể dùng cho đúng một định nghĩa hàm Do đó, khi có một lời gọi tới hàm foo(), chỉ có thể có đúng một định nghĩa khớp với tên hàm đó. int main() { ... foo(); ... } Function call binding Trong C++, đa hình hàm phức tạp hơn Thứ nhất, đa hình hàm (hàm chồng) buộc trình biên dịch phải kiểm tra cả danh sách tham số cùng tên hàm khi xác định liên kết int main() { ... foo(); ... foo(10); ... } Method call binding Thứ hai, lớp và phương thức làm rắc rối thêm vấn đề, và quy trình được gọi là method call binding – liên kết lời gọi phương thức Đối với các lớp độc lập (không thuộc cây thừa kế nào), quy trình này gần như không khác với function call binding so sánh tên phương thức, danh sách tham số để tìm định nghĩa tương ứng một trong số các tham số là tham số ẩn: con trỏ this Static binding – liên kết tĩnh Các trường hợp đó đều tương tự và đơn giản liên quan như thế nào đến chuyện bò điên? Các trường hợp đó, C/C++ function call binding, và C++ method binding cơ bản đều là ví dụ của static function call binding Static function call binding (hoặc static binding – liên kết tĩnh) là quy trình liên kết một lời gọi hàm với một định nghĩa hàm tại thời điểm biên dịch. do đó còn gọi là “compile-time binding” – liên kết khi biên dịch, hoặc “early binding” – liên kết sớm Hiểu rõ liên kết tĩnh – khi nào/tại sao nó xảy ra, khi nào/tại sao nó gây rắc rối – là chìa khoá để tìm được cách làm cho bò điên lúc nào cũng điên, và quan trọng hơn, làm thế nào để có đa hình. Static binding Với liên kết tĩnh, quyết định “định nghĩa hàm nào được chạy” được đưa ra tại thời điểm biên dịch - rất lâu trước khi chương trình chạy. thích hợp cho các lời gọi hàm thông thường mỗi lời gọi hàm chỉ xác định duy nhất một định nghĩa hàm, kể cả trường hợp hàm chồng. phù hợp với các lớp độc lập không thuộc cây thừa kế nào mỗi lời gọi phương thức từ một đối tượng của lớp hay từ con trỏ đến đối tượng đều xác định duy nhất một phương thức int main() { MyClass bar; ... bar.foo(); ... } class MyClass { public: void foo(); ... }; int main() { MyClass* bar; ... bar->foo(); ... } Static binding Trường hợp có cây thừa kế Cow bob(“Bob”,”brow”); MadCow maddy(“Maddy”, “white”); bob.makeASound();	// go “moo” maddy.makeASound();	// go “oap” MadCow& rMaddy = maddy; rMaddy.makeASound(); 	// Go “oap” MadCow* pMaddy = &maddy; pMaddy->makeASound(); 	// Go “oap” Cow& rCow = maddy; rCow.makeASound(); 	// Go “moo” Cow* pCow = &maddy; pCow->makeASound(); 	// Go “moo” Ta muốn in ra “oap” Static binding Ta muốn: rCow.makeASound() và pCow->makeASound() sẽ gọi MadCow::makeASound() hay Cow::makeASound() tuỳ theo rCow và pCow đang trỏ tới đối tượng Cow hay MadCow Thực tế xảy ra: với liên kết tĩnh, khi trình biên dịch sinh lời gọi hàm, nó thấy kiểu tĩnh của rCow là Cow&, nên gọi Cow::makeASound(). Static binding Liên kết tĩnh – Static binding: liên kết một tên hàm với phương thức thích hợp được thực hiện bởi việc phân tích tĩnh mã chương trình tại thời gian dịch dựa vào kiểu tĩnh (được khai báo) của biến tham gia lời gọi hàm. Liên kết tĩnh không quan tâm đến chuyện con trỏ (hoặc tham chiếu) có thể trỏ tới một đối tượng của một lớp dẫn xuất Với liên kết tĩnh, địa chỉ đoạn mã cần chạy cho một lời gọi hàm cụ thể là không đổi trong suốt thời gian chương trình chạy Static polymorphism Static polymorphism – đa hình tĩnh là kiểu đa hình được cài đặt bởi liên kết tĩnh Đối với đa hình tĩnh, trình biên dịch xác định trước định nghĩa hàm/phương thức nào sẽ được thực thi cho một lời gọi hàm/phương thức nào. Static polymorphism Đa hình tĩnh thích hợp cho các phương thức: được định nghĩa tại một lớp, và được gọi từ một thể hiện của chính lớp đó (trực tiếp hoặc gián tiếp qua con trỏ) được định nghĩa tại một lớp cơ sở và được thừa kế public nhưng không bị override tại lớp dẫn xuất, và được gọi từ một thể hiện của lớp dẫn xuất đó trực tiếp hoặc gián tiếp qua con trỏ tới lớp dẫn xuất, hoặc qua một con trỏ tới lớp cơ sở được định nghĩa tại một lớp cơ sở và được thừa kế public và bị override tại lớp dẫn xuất, và được gọi từ một thể hiện của lớp dẫn xuất đó (trực tiếp hoặc gián tiếp qua con trỏ tới lớp dẫn xuất) Static polymorphism Ta gặp rắc rối với đa hình tĩnh (như trong trường hợp bò điên), khi ta muốn gọi định nghĩa đã được override tại lớp dẫn xuất của một phương thức qua con trỏ tới lớp cơ sở. Ta muốn trình biên dịch hoãn quyết định gọi phương thức nào cho lời gọi hàm trên cho đến khi chương trình thực sự chạy. Cần một cơ chế cho phép xác định kiểu động tại thời gian chạy (tại thời gian chạy, chương trình có thể xác định con trỏ đang thực sự trỏ đến cái gì) Vậy, ta cần đa hình động – dynamic polymorphism Dynamic Binding – liên kết động Dynamic function call binding (dynamic binding – liên kết động) là quy trình liên kết một lời gọi hàm với một định nghĩa hàm tại thời gian chạy còn gọi là “run-time” binding hoặc “late binding” Với liên kết động, quyết định chạy định nghĩa hàm nào được đưa ra tại thời gian chạy, khi ta biết chắc con trỏ đang trỏ đến đối tượng thuộc lớp nào. Đa hình động (dynamic polymorphism) là loại đa hình được cài đặt bởi liên kết động. Virtual function – hàm ảo Hàm/phương thức ảo – virtual function/method là cơ chế của C++ cho phép cài đặt đa hình động Nếu khai báo một hàm thành viên (phương thức) là virtual, trình biên dịch sẽ đẩy lùi việc liên kết các lời gọi phương thức đó với định nghĩa hàm cho đến khi chương trình chạy. nghĩa là, ta bảo trình biên dịch sử dụng liên kết động thay cho liên kết tĩnh đối với phương thức đó Để một phương thức được liên kết tại thời gian chạy, nó phải khai báo là phương thức ảo (từ khoá virtual) tại lớp cơ sở Hàm ảo Hàm ảo Hàm ảo là phương thức được khai báo với từ khoá virtual trong định nghĩa lớp (nhưng không cần tại định nghĩa hàm, nếu định nghĩa hàm nằm ngoài định nghĩa lớp) Một khi một phương thức được khai báo là hàm ảo tại lớp cơ sở, nó sẽ tự động là hàm ảo tại mọi lớp dẫn xuất trực tiếp hoặc gián tiếp. Tuy không cần tiếp tục dùng từ khoá virtual trong các lớp dẫn xuất, nhưng vẫn nên dùng để tăng tính dễ đọc của các file header. nhắc ta và những người dùng lớp dẫn xuất của ta rằng phương thức đó sử dụng liên kết động class Cow { public: ... virtual void makeASound(); ... }; ... void Cow::makeASound() {... } class MadCow : public Cow { public: ... virtual void makeASound(); ... }; ... void MadCow::makeASound() {... } Các hàm makeASound() của Cow và các lớp dẫn xuất đều là hàm ảo Không cần nhưng nên có Hàm ảo đa hình động dễ cài và cần thiết. Vậy tại sao lại cần đa hình tĩnh? Trong Java, mọi phương thức đều mặc định là phương thức ảo, tại sao C++ không như vậy? Có hai lý do: tính hiệu quả C++ cần chạy nhanh, trong khi liên kết động tốn thêm phần xử lý phụ, do vậy làm giảm tốc độ Ngay cả những hàm ảo không được override cũng cần xử lý phụ  vi phạm nguyên tắc của C++: chương trình không phải chạy chậm vì những tính năng không dùng đến tính đóng gói khi khai báo một phương thức là phương thức không ảo, ta có ý rằng ta không định để cho phương thức đó bị override. Hàm ảo – constructor và destructor Không thể khai báo các constructor ảo constructor không được thừa kế, bao giờ cũng phải định nghĩa lại nên chuyện ảo không có nghĩa Có thể (và rất nên) khai báo destructor là hàm ảo. Destructor ảo Nhớ lại cây thừa kế MotorVehicle, nếu ta tạo và huỷ một đối tượng Car qua một con trỏ tới Car, mọi việc đều xảy ra như mong đợi Car* c = new Car(10, “Suzuki”, “RSX-R1000”, 4); ... delete c; 	// This works correctly – it will trigger 	// the Car destructor and then works 	// up to the MotorVehicle destructor Destructor ảo Còn nếu ta dùng một con trỏ tới MotorVehicle thay cho con trỏ tới Car, chỉ có destructor của MotorVehicle được gọi. Car* c = new Car(10, “Suzuki”, “RSX-R1000”, 4); MotorVehicle* mv = c; // Upcasting delete mv; 	// With static polymorphism, the compiler 	// thinks that this is referring to an 	// instance of MotorVehicle, so only the 	// base class (MotorVehicle) destructor is 	// invoked Destructor ảo Chú ý: việc gọi nhầm destructor không ảnh hưởng đến việc thu hồi bộ nhớ trong mọi trường hợp, phần bộ nhớ của đối tượng sẽ được thu hồi chính xác Trong ví dụ trước, kể cả nếu chỉ có destructor của MotorVehicle được gọi, phần bộ nhớ của toàn bộ đối tượng Car vẫn được thu hồi Tuy nhiên, nếu không gọi đúng destructor, các đoạn mã dọn dẹp quan trọng có thể bị bỏ qua chẳng hạn xoá các thành viên được cấp phát động Destructor ảo Quy tắc chung: mỗi khi tạo một lớp để được dùng làm lớp cơ sở, ta nên khai báo destructor là hàm ảo. kể cả khi destructor của lớp cơ sở rỗng (không làm gì) Vậy ta sẽ sửa lại lớp MotorVehicle như sau: class MotorVehicle { public: MotorVehicle(int vin, string make, string model); virtual ~MotorVehicle(); ... 

File đính kèm:

  • pptBài giảng Lập trình hướng đối tượng - Đa hình Hàm ảo.ppt