Bài giảng Lập trình hướng đối tượng - Thừa kế 2

Ta đã làm quen với các khái niệm cơ bản của hướng đối tượng, trong đó có hai cột trụ của tam giác OOP: đóng gói và thừa kế

Trong phần này, ta sẽ tiếp tục tìm hiểu về thừa kế và khái niệm này thể hiện trong C++ như thế nào

Kết thúc phần này, ta sẽ có đủ kiến thức chuẩn bị cho cột trụ thứ ba của OOP: đa hình

 

ppt31 trang | Chuyên mục: Lập Trình Hướng Đối Tượng | Chia sẻ: dkS00TYs | Lượt xem: 2415 | Lượt tải: 2download
Tóm tắt nội dung Bài giảng Lập trình hướng đối tượng - Thừa kế 2, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
kế có mọi thuộc tính và hành vi của lớp cha, cộng thêm các thuộc tính và hành vi của riêng lớp con đó Theo ngôn ngữ của C++: một thể hiện của một lớp con có thể truy nhập tới: mọi thuộc tính và hành vi (không phải private) của lớp cha, và các thành viên được định nghĩa riêng cho lớp con đó. Cái gì là cái gì? Như vậy, một thể hiện của Car có quyền truy nhập các thuộc tính và hành vi sau: thành viên dữ liệu: vin, make, model, passengers phương thức: drive() Ngược lại, không có lý gì một thể hiện của lớp cha lại có quyền truy nhập tới thuộc tính/hành vi chỉ được định nghĩa trong lớp con MotorVehicle không thể truy nhập passengers Tương tự, các lớp anh-chị-em không thể truy nhập các thuộc tính/hành vi của nhau Một đối tượng Car không thể có phương thức Load() và UnLoad(), cũng như một đối tượng Truck không thể có passengers C++ đảm bảo các yêu cầu đó như thế nào? Các đối tượng được thừa kế trong C++ Trong cây thừa kế MotorVehicle, giả sử mọi thành viên dữ liệu đều được khai báo protected, và ta sử dụng kiểu thừa kế public Lớp cơ sở MotorVehicle: class MotorVehicle { public: MotorVehicle(int vin, string make, string model); ~MotorVehicle(); void Drive(int speed, int distance); protected: int vin; string make; string model; }; Các đối tượng được thừa kế trong C++ Các lớp dẫn xuất Car và Truck: class Car : public MotorVehicle { public: Car(int vin, string make, string model, int passengers); ~Car(); protected: int passengers; }; class Truck : public MotorVehicle { public: Truck(int vin, string make, string model, int maxPayload); ~Truck(); void Load(); void Unload(); protected: int maxPayload; }; Các đối tượng được thừa kế trong C++ Ta có thể khai báo các thể hiện của các lớp đó và sử dụng chúng như thế nào? Thí dụ, khai báo các con trỏ tới 3 lớp: MotorVehicle* mvPointer; Car* cPointer; Truck* tPointer; ... mvPointer = new MotorVehicle(10, “Honda”, “S2000”); cPointer = new Car(10, “Honda”, “S2000”, 2); tPointer = new Truck(10, “Toyota”, “Tacoma”, 5000); ... Sử dụng các con trỏ để khai báo các đối tượng thuộc các lớp tương ứng Các đối tượng được thừa kế trong C++ Trong cả ba trường hợp, ta có thể truy nhập các phương thức của lớp cha, do ta đã sử dụng kiểu thừa kế public Thí dụ: mvPointer->Drive(); 	// Method defined by this class cPointer->Drive(); 	// Method defined by base class tPointer->Drive(); 	// Method defined by base class mvPointer->Load(); 	// Error cPointer->Load(); 	// Error tPointer->Load(); 	// Method defined by this class Tuy nhiên, các phương thức định nghĩa tại một lớp dẫn xuất chỉ có thể được truy nhập bởi lớp đó xét phương thức Load() của lớp Truck Upcast Các thể hiện của lớp con thừa kế public có thể được đối xử như thể nó là thể hiện của lớp cha. từ thể hiện của lớp con, ta có quyền truy nhập các thành viên và phương thức public mà ta có thể truy nhập từ một thể hiện của lớp cha. Do đó, C++ cho phép dùng con trỏ được khai báo thuộc loại con trỏ tới lớp cơ sở để chỉ tới thể hiện của lớp dẫn xuất ta có thể thực hiện các lệnh sau: MotorVehicle* mvPointer2; mvPointer2 = mvPointer; 	// Point to another MotorVehicle mvPointer2 = cPointer; 	// Point to a Car mvPointer2 = tPointer; 	// Point to a Truck Upcast Điều đáng lưu ý là ta thực hiện tất cả các lệnh gán đó mà không cần đổi kiểu tường minh do mọi lớp con của MotorVehicle đều chắc chắn có mọi thành viên và phương thức có trong một MotorVehicle, việc tương tác với thể hiện của các lớp này như thể chúng là MotorVehicle không có chút rủi ro nào Ví dụ, lệnh sau đây là hợp lệ, bất kể mvPointer2 đang trỏ tới một MotorVehicle, một Car, hay một Truck mvPointer2->Drive(); Upcast Upcast là quá trình tương tác với thể hiện của lớp dẫn xuất như thể nó là thể hiện của lớp cơ sở. Cụ thể, đây là việc đổi một con trỏ (hoặc tham chiếu) tới lớp dẫn xuất thành một một con trỏ (hoặc tham chiếu) tới lớp cơ sở ta đã thấy ví dụ về upcast đối với con trỏ MotorVehicle* mvPointer2 = cPointer; ví dụ về upcast đối với tham chiếu // Refer to the instance pointed to by cPointer MotorVehicle& mvReference = *cPointer; // Refer to an automatically-allocated instance c Car c(10, “Honda”, “S2000”, 2); MotorVehicle& mvReference2 = c; Upcast Upcast thường gặp tại các định nghĩa hàm, khi một con trỏ/tham chiếu đến lớp cơ sở được yêu cầu, nhưng con trỏ/tham chiếu đến lớp dẫn xuất cũng được chấp nhận xét hàm sau void sellMyVehicle(MotorVehicle& myVehicle) {...} có thể gọi sellMyVehicle một cách hợp lệ với tham số là một tham chiếu tới một MotorVehicle, một Car, hoặc một Truck. Upcast Nếu ta dùng một con trỏ tới lớp cơ sở để trỏ tới một thể hiện của lớp dẫn xuất, trình biên dịch sẽ chỉ cho ta coi đối tượng như thể nó thuộc lớp cơ sở Như vậy, ta không thể làm như sau MotorVehicle* mvPointer2 = tPointer; // Point to a Truck mvPointer2->Load(); 	// Error Đó là vì trình biên dịch không thể đảm bảo rằng con trỏ thực ra đang trỏ tới một thể hiện của Truck. Upcast Chú ý rằng khi gắn một con trỏ/tham chiếu lớp cơ sở với một thể hiện của lớp dẫn xuất, ta không hề thay đổi bản chất của đối tượng được trỏ tới Ví dụ, lệnh MotorVehicle* mvPointer2 = tPointer; // Point to a Truck không làm một thể hiện của Truck suy giảm thành một MotorVehicle, nó chỉ cho ta một cách nhìn khác đối với đối tượng Truck và tương tác với đối tượng đó. Do vậy, ta vẫn có thể truy nhập tới các thành viên và phương thức của lớp dẫn xuất ngay cả sau khi gán con trỏ lớp cơ sở tới nó: mvPointer2 = tPointer; 	// Point to a Truck tPointer->Load(); 	// We can still do this mvPointer2->Load(); 	// Even though we can’t do this (error) Slicing Đôi khi ta muốn đổi hẳn kiểu Ví dụ, ta muốn tạo một thể hiện của MotorVehicle dựa trên một thể hiện của Car (sử dụng copy constructor cho MotorVehicle) Slicing là quá trình chuyển một thể hiện của lớp dẫn xuất thành một thể hiện của lớp cơ sở hợp lệ vì một thể hiện của lớp dẫn xuất có tất cả các thành viên và phương thức của lớp cơ sở của nó Quy trình này gọi là “slicing” vì thực chất ta cắt bớt (slice off) những thành viên dữ liệu và phương thức được định nghĩa trong lớp dẫn xuất Slicing Ví dụ Car c(10, “Honda”, “S2000”, 2)’ MotorVehicle mv(c); Ở đây, một thể hiện của MotorVehicle được tạo bởi copy constructor chỉ giữ lại những thành viên của Car mà có trong MotorVehicle (Car instance) (MotorVehicle instance) sliced off Slicing Thực ra, quy trình này cũng giống hệt như khi ta ngầm đổi giữa các kiểu dữ liệu có sẵn và bị mất bớt dữ liệu (chẳng hạn khi đổi một số chấm động sang số nguyên) Slicing còn xảy ra khi ta dùng phép gán Car c(10, “Honda”, “S2000”, 2)’ MotorVehicle mv = c; Downcast Upcast là đổi con trỏ/tham chiếu tới lớp dẫn xuất thành con trỏ/tham chiếu tới lớp cơ sở. Downcast là quy trình ngược lại: đổi kiểu con trỏ/tham chiếu tới lớp cơ sở thành con trỏ/tham chiếu tới lớp dẫn xuất. downcast là quy trình rắc rối hơn và có nhiều điểm không an toàn Downcast Trước hết, downcast không phải là một quy trình tự động - nó luôn đòi hỏi đổi kiểu tường minh (explicit type cast) Điều đó là hợp lý nhớ lại rằng: không phải “mọi xe chạy bằng máy đều là xe tải” do đó, rắc rối sẽ nảy sinh nếu trình biên dịch cho ta đổi một con trỏ bất kỳ tới MotorVehicle thành một con trỏ tới Truck, trong khi thực ra con trỏ đó đang trỏ tới một đối tượng Car. Ví dụ, đoạn mã sau sẽ gây lỗi biên dịch: MotorVehicle* mvPointer3; … Car* cPointer2 = mvPointer3; 	// Error Truck* tPointer2 = mvPointer3; 	// Error MotorCycle mcPointer2 = mvPointer3; 	// Error Downcast Nếu ta biết chắc chắn rằng một con trỏ lớp cơ sở quả thực đang trỏ tới một lớp con, ta có thể tự đổi kiểu cho con trỏ lớp cơ sở bằng cách sử dụng static_cast Car* cPointer = new Car(10, “Honda”, “S2000”, 2); MotorVehicle* mv = cPointer; 	 // Upcast Car* cPointer2; cPointer2 = static_cast(mv); // Explicit downcast Downcast Đoạn mã trên hoàn toàn hợp lệ và sẽ được trình biên dịch chấp nhận Tuy nhiên, nếu chạy đoạn trình trên, chương trình có thể bị đổ vỡ (thường là khi lần đầu truy nhập đến thành viên/phương thức được định nghĩa của lớp dẫn xuất mà ta đổi tới) Truck* tPointer = new Truck(10, “Toyota”, “Tacoma”, 5000); MotorVehicle* mv = tPointer; // Upcast Car* cPointer2; cPointer = static_cast(mv); // Explicit downcast Đa thừa kế - Multiple Inheritance Đa thừa kế là dạng thừa kế phức tạp cho phép ta tạo các lớp dẫn xuất thừa kế các thuộc tính và hành vi từ nhiều hơn một lớp cơ sở. Nên hạn chế sử dụng đa thừa kế vì nó có thể dẫn đến đủ loại nhầm lẫn do các tên trùng nhau chẳng hạn nếu thừa kế từ hai lớp cùng có phương thức Foo() Do tính phức tạp tiềm tàng của đa thừa kế, một số ngôn ngữ lập trình (chẳng hạn Java, C#) không cài đặt tính năng này. Đa thừa kế Xét một hệ thống quản lý đại học lưu trữ thông tin về cả sinh viên và nhân viên sinh viên có thể có các thuộc tính chẳng hạn tên, mã sinh viên, và điểm trung bình nhân viên có thể có các thuộc tính tên, mã nhân viên, phòng làm việc Có thể có những người cùng lúc vừa là sinh viên vừa là nhân viên có thể sinh viên-nhân viên có thêm thuộc tính nợ học phí (tuition credit) Đa thừa kế Ta có thể thấy các trùng lặp tiềm tàng trong các thành viên dữ liệu, chẳng hạn lớp Student-Employee thừa kế thành viên name từ cả hai lớp cơ sở trong C++, có thể giải quyết rắc rối do trùng tên này bằng toán tử phạm vi (cho trình biên dịch biết ta đang nói đến name nào) Đa thừa kế Ví dụ về đa thừa kế trong cây thừa kế Nguyên lý thừa kế vẫn không đổi: lớp con thừa kế mọi thuộc tính và hành vi của lớp cha Trong C++, điều đó có nghĩa lớp dẫn xuất thừa kế mọi thành viên dữ liệu và phương thức của từng lớp cơ sở Đa thừa kế - cài đặt Khai báo một lớp là dẫn xuất của nhiều lớp cơ sở: class StudentEmployee : public Student, public Employee { ... }; Đa thừa kế Trong thực tế, có một loạt các vấn đề tiềm tàng liên quan đến đa thừa kế, phần lớn là do rắc rối vì mù mờ Có thể ta không bao giờ cần dùng đến đa thừa kế, nhưng cũng có những tình huống mà đa thừa kế là lời giải tốt nhất (và có thể là duy nhất) Nếu sử dụng đa thừa kế, nhất thiết phải cân nhắc về các xung đột có thể nảy sinh trong khi sử dụng các lớp có liên quan. 

File đính kèm:

  • pptBài giảng Lập trình hướng đối tượng - Thừa kế 2.ppt