Bài giảng Ngôn ngữ lập trình - Bài 9: Đa hình và Hàm ảo - Lý Anh Tuấn

pdf 35 trang huongle 3440
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Ngôn ngữ lập trình - Bài 9: Đa hình và Hàm ảo - Lý Anh Tuấn", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfbai_giang_ngon_ngu_lap_trinh_bai_9_da_hinh_va_ham_ao_ly_anh.pdf

Nội dung text: Bài giảng Ngôn ngữ lập trình - Bài 9: Đa hình và Hàm ảo - Lý Anh Tuấn

  1. NGÔN NGỮ LẬP TRÌNH Bài 9: Đa hình và Hàm ảo Giảng viên: Lý Anh Tu ấn Email: tuanla@tlu.edu.vn
  2. Nội dung 1. Cơ bản về hàm ảo ◦ Kết gán muộn ◦ Thi hành hàm ảo ◦ Khi nào sử dụng hàm ảo ◦ Lớp trừu tượng và hàm ảo thuần túy 2. Con trỏ và hàm ảo ◦ Sự tương thích kiểu mở rộng ◦ Ép lên và ép xuống ◦ Chi tiết hơn về hàm ảo 2
  3. Cơ bản về hàm ảo  Đa hình ◦ Liên kết nhiều ngữ nghĩa với một hàm ◦ Hàm ảo cung cấp khả năng này ◦ Là nguyên tắc cơ bản của lập trình hướng đối tượng  Ảo ◦ Tồn tại về bản chất mặc dù trên thực tế không tồn tại  Hàm ảo ◦ Có thể được sử dụng trước khi được định nghĩa 3
  4. Ví dụ hình vẽ  Lớp của một số kiểu hình vẽ ◦ Hình chữ nhật (rectangle), hình tròn (circle), hình ovan (oval), vân vân ◦ Mỗi hình vẽ là một đối tượng của các lớp khác nhau  Dữ liệu hình chữ nhật: độ cao, chiều rộng, tâm điểm  Dữ liệu hình tròn: tâm điểm, bán kính  Tất cả dẫn xuất từ một lớp cha: Figure  Hàm cần thiết: draw() ◦ Các chỉ thị khác nhau cho mỗi hình vẽ 4
  5. Ví dụ hình vẽ: center()  Mỗi lớp cần một hàm draw khác nhau  Có thể gọi draw trong mỗi lớp: Rectangle r; Circle c; r.draw(); //Gọi hàm draw của lớp Rectangle c.draw(); // Gọi hàm draw của lớp Circle  Lớp cha Figure bao gồm các hàm áp dụng cho tất cả các hình vẽ; chẳng hạn: center(): di chuyển hình vẽ vào tâm của màn hình ◦ Xóa hình ban đầu, sau đó vẽ lại ◦ Do vậy Figure::center() sẽ gọi hàm draw để vẽ lại ◦ Vấn đề: Gọi hàm draw() từ lớp nào? 5
  6. Ví dụ hình vẽ: Hình mới  Xét kiểu hình vẽ mới như sau: lớp Triangle được dẫn xuất từ lớp Figure  Hàm center() được kế thừa từ Figure ◦ Nó có làm việc với các triangle không? ◦ Nó sử dụng draw() khác với các hình khác ◦ Nó sẽ sử dụng Figure::draw() không làm việc với các triangle  Cần hàm center() được kế thừa sử dụng hàm Triangle::draw() chứ không phải hàm Figure::draw() ◦ Nhưng lớp Triangle thậm chí còn chưa được viết khi viết Figure::center() 6
  7. Ví dụ hình vẽ: Hình ảo  Câu trả lời là sử dụng hàm ảo  Nói cho bộ biên dịch: ◦ Không biết hàm được thi hành như thế nào ◦ Đợi cho đến khi sử dụng chương trình ◦ Sau đó nhận thi thành từ bản thể đối tượng  Được gọi là kết gán muộn hoặc kết gán động ◦ Hàm ảo thi hành kết gán muộn 7
  8. Một ví dụ khác  Chương trình lưu trữ hồ sơ cửa hàng phụ tùng ô tô ◦ Theo dõi các giao dịch ◦ Chưa biết biết tất cả các giao dịch ◦ Ban đầu chỉ có các giao dịch bán lẻ ◦ Sau đó: Giao dịch giảm giá, thư đặt hàng, vân vân  Phụ thuộc vào các nhân tố khác bên cạnh giá và thuế 8
  9. Hàm ảo: Phụ tùng ô tô  Chương trình phải: ◦ Tính toán doanh thu tổng cộng hàng ngày ◦ Tính toán giao dịch lớn nhất/nhỏ nhất của ngày ◦ Có thể tính giá trị trung bình của các giao dịch của ngày  Tất cả đến từ các hóa đơn riêng lẻ ◦ Nhưng nhiều hàm tính toán hóa đơn sẽ được thêm vào sau  Khi các kiểu giao dịch khác được thêm vào  Do vậy hàm tính toán hóa đơn sẽ là ảo 9
  10. Định nghĩa lớp Sale  class Sale { public: Sale(); Sale(double thePrice); double getPrice() const; virtual double bill() const; double savings(const Sale& other) const; private: double price; }; 10
  11. Hàm thành viên: savings và toán tử <  double Sale::savings(const Sale& other) const { return (bill() – other.bill()); }  bool operator < ( const Sale& first, const Sale& second) { return (first.bill() < second.bill()); }  Lưu ý cả hai đều sử dụng hàm thành viên bill() 11
  12. Lớp Sale  Biểu diễn các giao dịch của mục riêng lẻ không tính giảm giá hoặc phụ phí  Lưu ý giữ nguyên từ virtual trong khai báo của hàm thành viên bill ◦ Hiệu quả: Sau này, các lớp dẫn xuất của Sale có thể định nghĩa phiên bản hàm hóa đơn của chúng ◦ Các hàm thành viên khác của Sale sẽ sử dụng phiên bản dựa trên đối tượng của lớp dẫn xuất ◦ Chúng sẽ không tự động sử dụng phiên bản của Sale 12
  13. Định nghĩa lớp dẫn xuất DiscountSale  class DiscountSale : public Sale { public: DiscountSale(); DiscountSale( double thePrice, double the Discount); double getDiscount() const; void setDiscount(double newDiscount); double bill() const; private: double discount; }; 13
  14. Thi hành DiscoutSale của bill()  Hàm ảo trong lớp cơ sở ◦ Tự động ảo trong lớp dẫn xuất  Khai báo lớp dẫn xuất (trong giao diện) ◦ Không đòi hỏi có từ khóa “virtual” ◦ Nhưng thường được thêm vào cho dễ đọc 14
  15. Lớp dẫn xuất DiscountSale  Hàm thành viên bill() của DiscountSale thi hành khác với của Sale ◦ Đặc biệt với việc giảm giá  Các hàm thành viên savings và “<“ ◦ Sẽ sử dụng định nghĩa này của bill cho tất cả các đối tượng của lớp DiscountSale ◦ Thay vì để mặc định với phiên bản được định nghĩa trong lớp Sale 15
  16. Hàm ảo  Lớp Sale được viết trước lớp dẫn xuất DiscountSale ◦ Các hàm thành viên savings và “<“ được biên dịch thậm chí trước khi có ý tưởng về lớp DiscountSale  Do vậy trong lời gọi chẳng hạn như: DiscountSale d1, d2; d1.savings(d2); ◦ Lời gọi trong savings() tới hàm bill() biết sử dụng định nghĩa bill() từ lớp DiscountSale  Rất hữu ích 16
  17. Hàm ảo  Điều kỳ diệu khi viết các chương trình C++  Giải thích về kết gán muộn ◦ Hàm ảo thi hành kết gán muộn ◦ Nói với bộ biên dịch đợi cho đến khi hàm được sử dụng trong chương trình ◦ Quyết định định nghĩa nào được sử dụng dựa vào việc gọi đối tượng  Là quy tắc lập trình hướng đối tượng rất quan trọng 17
  18. Ghi đè  Định nghĩa hàm ảo thay đổi trong lớp dẫn xuất ◦ Chúng ta nói là nó được ghi đè  Tương tự như được định nghĩa lại cho các hàm chuẩn  Do vậy: ◦ Hàm ảo bị thay đổi: ghi đè ◦ Hàm không phải hàm ảo bị thay đổi: định nghĩa lại 18
  19. Hàm ảo: Nhược điểm  Chúng ta đã biết các ưu điểm của hàm ảo  Một nhược điểm chính: phụ phí ◦ Sử dụng nhiều bộ nhớ hơn ◦ Kết gán muộn là “trong khi chạy”, do vậy chương trình chạy chậm hơn  Do vậy nếu không nên sử dụng hàm ảo nếu không thực sự cần thiết 19
  20. Hàm ảo thuần túy  Lớp cơ sở có thể không có định nghĩa có ý nghĩa cho thành viên nào đó của nó ◦ Mục đích chỉ để các lớp khác kế thừa  Nhắc lại lớp Figure ◦ Tất cả các hình vẽ là các đối tượng của các lớp dẫn xuất:  Hình chữ nhật, hình tròn, hình tam giác, vân vân. ◦ Lớp Figure không có ý tưởng về việc vẽ như thế nào  Đặt nó là một hàm ảo thuần túy: virtual void draw() = 0; 20
  21. Lớp cơ sở trừa tượng  Hàm ảo thuần túy không đòi hỏi phải định nghĩa ◦ Bắt tất cả các lớp dẫn xuất định nghĩa phiên bản của riêng chúng  Lớp với một hoặc nhiều hàm ảo thuần túy là: lớp cơ sở trùy tượng ◦ Có thể được sử dụng như lớp cơ sở ◦ Không có đối tượng nào có thể được tạo ra từ nó  Vì nó không có các định nghĩa hoàn chỉnh cho tất cả các thành viên của nó  Nếu lớp dẫn xuất thất bại trong việc định nghĩa tất cả các hàm ảo thuần túy ◦ Nó cũng là lớp cơ sở trừu tượng 21
  22. Tương thích kiểu mở rộng  Biết rằng: Derived là lớp dẫn xuất của Base ◦ Các đối tượng Derived có thể được gán cho các đối tượng kiểu Base ◦ Nhưng ngược lại thì không  Xét ví dụ trước: ◦ Một DiscountSale là một Sale nhưng ngược lại thì không đúng 22
  23. Ví dụ tương thích kiểu mở rộng  class Pet { public: string name; virtual void print() const; }; class Dog : public Pet { public: string breed; virtual void print() const; }; 23
  24. Lớp Pet và Dog  Bây giờ cung cấp khai báo: Dog vdog; Pet vpet;  Lưu ý các biến thành viên name và breed là public ◦ Chỉ nhằm mục đích ví dụ, không điển hình 24
  25. Sử dụng lớp Pet và Dog  Một chú chó cũng là một thú cưng: ◦ vdog.name = "Tiny"; vdog.breed = "Great Dane"; vpet = vdog; ◦ Những lệnh này được phép  Có thể gán các giá trị cho kiểu cha, nhưng ngược lại thì không ◦ Một thú cưng không phải là một chú chó 25
  26. Vấn đề tách lớp  Lưu ý rằng giá trị được gán cho vpet mất trường breed của nó ◦ cout<<vpet.breed;  Tạo ra thông điệp lỗi ◦ Được gọi là vấn đề tách lớp  Dường như thích hợp ◦ Dog được chuyển thành biến Pet, do vậy nó sẽ được đối xử như một Pet  Và do vậy không có các tính chất của chó ◦ Tạo ra tranh cãi triết học thú vị 26
  27. Chỉnh sửa vấn đề tách lớp  Trong C++, vấn đề tách lớp là trở ngại ◦ Nó vẫn là một Great Dane tên là Tiny ◦ Chúng ta muốn nói tới giống của nó thậm chí khi nó được đối xử như một Pet  Có thể làm như vậy bằng các con trỏ trỏ tới các biến động 27
  28. Ví dụ vấn đề tách lớp  Pet *ppet; Dog *pdog; pdog = new Dog; pdog->name = "Tiny"; pdog->breed = "Great Dane"; ppet = pdog;  Không thể truy cập trường breed của đối tượng được trỏ tới bởi ppet: cout breed; //Khong hop le 28
  29. Ví dụ vấn đề tách lớp  Phải sử dụng hàm thành viên ảo: ppet->print(); ◦ Gọi hàm thành viên print trong lớp Dog  Bởi vì nó là ảo  C++ đợi để xem đối tượng nào con trỏ ppet đang trỏ tới trước khi gọi kết gán. 29
  30. Hàm hủy ảo  Nhắc lại: hàm hủy cần để hủy cấp phát dữ liệu được cấp phát động  Xét: Base *pBase = new Derived; delete pBase; ◦ Sẽ gọi hàm hủy lớp cơ sở mặc dù đang trỏ tới đối tượng lớp Derived ◦ Khai báo hàm hủy là virtual để chỉnh sửa vấn đề này  Có thể để tất cả các hàm hủy là ảo 30
  31. Ép kiểu  Xét: Pet vpet; Dog vdog; vdog = static_cast (vpet); //Không hợp lệ!  Không thể ép một thú cưng thành một chú chó, nhưng: vpet = vdog; // Legal! vpet = static_cast (vdog); //Also legal!  Ép kiểu lên thực hiện được ◦ Từ kiểu hậu duệ đến kiểu tổ tiên 31
  32. Ép kiểu xuống  Ép kiểu xuống khá nguy hiểm ◦ Ép từ kiểu tổ tiên xuống kiểu hậu duệ ◦ Giả sử thông tin được thêm vào ◦ Có thể thực hiện với dynamic_cast: Pet *ppet; ppet = new Dog; Dog *pdog = dynamic_cast (ppet);  Hợp lệ, nhưng nguy hiểm  Ép kiểu xuống hiếm khi được sử dụng do: ◦ Phải theo dõi tất cả thông tin được thêm vào ◦ Tất cả các hàm thành viên phải là ảo 32
  33. Công việc nội tại của hàm ảo  Không cần biết sử dụng nó như thế nào ◦ Quy tắc che dấu thông tin  Bảng hàm ảo ◦ Bộ biên dịch tạo ra nó ◦ Có các con trỏ cho mỗi hàm thành viên ảo ◦ Trỏ tới vị trí mã lệnh của hàm đó  Các đối tượng của các lớp như vậy cũng có con trỏ ◦ Các con trỏ trỏ tới bảng hàm ảo 33
  34. Tóm tắt  Kết gán muộn hoãn việc quyết định hàm thành viên nào được gọi cho đến khi chương trình chạy ◦ Trong C++ các hàm ảo sử dụng kết gán muộn  Các hàm ảo thuần túy không có định nghĩa ◦ Các lớp có ít nhất một hàm ảo thuần túy là trừu tượng ◦ Không có đối tượng nào có thể tạo ra từ lớp trừu tượng ◦ Được sử dụng làm cơ sở cho các lớp khác dẫn xuất 34
  35. Tóm tắt  Đối tượng lớp dẫn xuất có thể được gán cho đối tượng lớp cơ sở ◦ Các thành viên lớp cơ sở bị mất: vấn đề tách lớp  Phép gán con trỏ và các đối tượng động ◦ Cho phép chỉnh sửa vấn đề tách lớp  Khai báo tất cả các hàm hủy là ảo ◦ Áp dụng tốt cho việc lập trình ◦ Đảm bảo bộ nhớ được hủy cấp phát đúng 35