Phươngthứcảovàtươngứngbội 3.1. Cách định nghĩa phươngthứcảo Giả sử A là lớp cơ sở, các lớp B, C, D dẫn xuất (trực tiếp hoặc dán tiếp) từ A. Giả sử trong 4 lớp trên đều có các phươngthức trùng dòng tiêu đề (trùng kiểu, trùng tên, trùng các đối). Để định nghĩa các phươngthức này là các phươngthức ảo, ta chỉ cần: + Hoặc thêm từ khoá virtual vào dòng tiêu đề của phươngthức bên trong định nghĩa lớp cơ sở A. + Hoặc thêm từ khoá virtual vào dòng tiêu đề bên trong định nghĩa của tất cả các lớp A, B, C và D. Ví dụ: Cách 1: class A { . virtual void hien_thi() { cout << “\n Đây là lớp A” ; }; } ; class B : public A { . void hien_thi() { cout << “\n Đây là lớp B” ; }; } ; class C : public B { . void hien_thi() { cout << “\n Đây là lớp C” ; }; } ; class D : public A { . 329 330 void hien_thi() { cout << “\n Đây là lớp D” ; }; } ; Cách 2: class A { . virtual void hien_thi() { cout << “\n Đây là lớp A” ; }; } ; class B : public A { . virtual void hien_thi() { cout << “\n Đây là lớp B” ; }; } ; class C : public B { . virtual void hien_thi() { cout << “\n Đây là lớp C” ; }; } ; class D : public A { . virtual void hien_thi() { cout << “\n Đây là lớp D” ; }; } ; Chú ý: Từ khoá virtual không được đặt bên ngoài định nghĩa lớp. Ví dụ nếu viết như sau là sai (CTBD sẽ báo lỗi). class A 331 332 { . virtual void hien_thi() ; } ; virtual void hien_thi() // Sai { cout << “\n Đây là lớp A” ; }; Cần sửa lại như sau: class A { . virtual void hien_thi() ; } ; void hien_thi() // Đúng { cout << “\n Đây là lớp A” ; }; 3.2. Quy tắc gọi phươngthứcảo Để có sự so sánh với phươngthức tĩnh, ta nhắc lại quy tắc gọi phươngthức tĩnh nêu trong § 1. 3.2.1. Quy tắc gọi phươngthức tĩnh Lời gọi tới phươngthức tĩnh bao giờ cũng xác định rõ phươngthức nào (trong số các phươngthức trùng tên của các lớp có quan hệ thừa kế) được gọi: 1. Nếu lời gọi xuất phát từ một đối tượng của lớp nào, thì phươngthức của lớp đó sẽ được gọi. 2. Nếu lời gọi xuất phát từ một con trỏ kiểu lớp nào, thì phươngthức của lớp đó sẽ được gọi bất kể con trỏ chứa địa chỉ của đối tượng nào. 3.2.2. Quy tắc gọi phươngthứcảoPhươngthứcảo chỉ khác phươngthức tĩnh khi được gọi từ một con trỏ (trường hợp 2 nêu trong mục 3.2.1). Lời gọi tới phươngthứcảo từ một con trỏ chưa cho biết rõ phươngthức nào (trong số các phươngthứcảo trùng tên của các lớp có quan hệ thừa kế) sẽ được gọi. Điều này phụ thuộc vào đối tượng cụ thể mà con trỏ đang trỏ tới: Con trỏ đang trỏ tới đối tượng của lớp nào thì phươngthức của lớp đó sẽ được gọi. Ví dụ A, B, C và D là các lớp đã định nghĩa trong 3.1. Ta khai báo một con trỏ kiểu A và 4 đối tượng: A *p ; // p là con trỏ kiểu A A a ; // a là biến đối tượng kiểu A B b ; // b là biến đối tượng kiểu B C c ; // c là biến đối tượng kiểu C D d ; // d là biến đối tượng kiểu D Xét lời gọi tới các phươngthứcảo hien_thi sau: p = &a; // p trỏ tới đối tượng a của lớp A p->hien_thi() ; // Gọi tới A::hien_thi() p = &b; // p trỏ tới đối tượng b của lớp B p->hien_thi() ; // Gọi tới B::hien_thi() p = &c; // p trỏ tới đối tượng c của lớp C p->hien_thi() ; // Gọi tới C::hien_thi() p = &d; // p trỏ tới đối tượng d của lớp D p->hien_thi() ; // Gọi tới D::hien_thi() 3.3. Tươngứngbội Chúng ta nhận thấy cùng một câu lệnh p->hien_thi(); tươngứng với nhiều phươngthức khác nhau. Đây chính là tươngứng bội. Khả năng này rõ ràng cho phép xử lý nhiều đối tượng khác nhau, nhiều công việc, thậm chí nhiều thuật toán khác nhau theo cùng một cách thức, cùng một lược đồ. Điều này sẽ được minh hoạ trong các mục tiếp theo. 3.4. Liên kết động Có thể so sánh sự khác nhau giữ phươngthức tĩnh vàphươngthứcảo trên khía cạnh liên kết một lời gọi với một phương thức. Trở lại ví dụ trong 3.2: A *p ; // p là con trỏ kiểu A A a ; // a là biến đối tượng kiểu A B b ; // b là biến đối tượng kiểu B C c ; // c là biến đối tượng kiểu C D d ; // d là biến đối tượng kiểu D Nếu hien_thi() là các phươngthức tĩnh, thì dù p chứa địa chỉ của các đối tượng a, b, c hay d, thì lời gọi: p->hien_thi() ; luôn luôn gọi tới phươngthức A::hien_thi() Như vậy một lời gọi (xuất phát từ con trỏ) tới phươngthức tĩnh luôn luôn liên kết với một phươngthức cố định và sự liên kết này xác định trong quá trình biên dịch chương trình. Cũng với lời gọi: p->hien_thi() ; như trên, nhưng nếu hien_thi() là các phươngthức ảo, thì lời gọi này không liên kết cứng với một phươngthức cụ thể nào. Phươngthức mà nó liên kết (gọi tới) còn chưa xác định trong giai đoạn dịch chương trình. Lời gọi này sẽ: + liên kết với A::hien_thi() , nếu p chứa địa chỉ đối tượng lớp A 333 334 + liên kết với B::hien_thi() , nếu p chứa địa chỉ đối tượng lớp B + liên kết với C::hien_thi() , nếu p chứa địa chỉ đối tượng lớp C + liên kết với D::hien_thi() , nếu p chứa địa chỉ đối tượng lớp D Như vậy một lời gọi (xuất phát từ con trỏ) tới phươngthứcảo không liên kết với một phươngthức cố định, mà tuỳ thuộc vào nội dung con trỏ. Đó là sự liên kết động vàphươngthức được liên kết (được gọi) thay đổi mỗi khi có sự thay đổi nội dung con trỏ trong quá trình chạy chương trình. 3.5. Quy tắc gán địa chỉ đối tượng cho con trỏ lớp cơ sở + Như đã nói trong § 1, C++ cho phép gán địa chỉ đối tượng của một lớp dẫn xuất cho con trỏ của lớp cơ sở. Như vậy các phép gán sau (xem 3.2) là đúng: A *p ; // p là con trỏ kiểu A A a ; // a là biến đối tượng kiểu A B b ; // b là biến đối tượng kiểu B C c ; // c là biến đối tượng kiểu C D d ; // d là biến đối tượng kiểu D p = &a; // p và a cùng lớp A p = &b; // p là con trỏ lớp cơ sở, b là đối tượng lớp dẫn xuất p = &c; // p là con trỏ lớp cơ sở, c là đối tượng lớp dẫn xuất p = &d; // p là con trỏ lớp cơ sở, d là đối tượng lớp dẫn xuất + Tuy nhiên cần chú ý là: Không cho phép gán địa chỉ đối tượng của lớp cở sở cho con trỏ của lớp dẫn xuất. Như vậy ví dụ sau là sai: B *q ; A a ; q = &a; Sai vì: Gán địa chỉ đối tượng của lớp cơ sở A cho con trỏ của lớp dẫn xuất B 3.6. Ví dụ Ta sửa chương trình trong § 1 bằng cách định nghĩa các phươngthức xuat() là ảo. Khi đó bốn câu lệnh: hien(&a); hien(&b); hien(&c); hien(&d); trong hàm main (của chương trình dưới đây) sẽ lần lượt gọi tới 4 phươngthức khác nhau: A::xuat() B::xuat() C::xuat() D::xuat() //CT6-01B // Phuongthucảovàtươngứngbội #include <conio.h> 335 336 #include <stdio.h> #include <iostream.h> #include <ctype.h> class A { private: int n; public: A() { n=0; } A(int n1) { n=n1; } virtual void xuat() { cout << "\nLop A: "<< n; } int getN() { return n; } }; class B:public A { public: B():A() { } B(int n1):A(n1) { } void xuat() { cout << "\nLop B: "<<getN(); } }; class C:public A { public: C():A() { } C(int n1):A(n1) { } void xuat() { cout << "\nLop C: "<<getN(); } }; class D:public C { public: D():C() { } D(int n1):C(n1) { } void xuat() { cout << "\nLop D: "<<getN(); } }; void hien(A *p) { p->xuat(); } void main() { A a(1); B b(2); C c(3); D d(4); clrscr(); hien(&a); hien(&b); 337 338 hien(&c); hien(&d); getch(); } 3.5. Sự thừa kế của các phươngthứcảo Cũng giống như các phươngthức thông thường khác, phươngthứcảo cũng có tính thừa kế. Chẳng hạn trong chương trình trên (mục 3.4) ta bỏ đi phươngthức xuat() của lớp D, thì câu lệnh: hien(&d) ; (câu lệnh gần cuối trong hàm main) sẽ gọi tới C::xuat() , phươngthức này được kế thừa trong lớp D (vì D dẫn xuất từ C). . gọi phương thức ảo Phương thức ảo chỉ khác phương thức tĩnh khi được gọi từ một con trỏ (trường hợp 2 nêu trong mục 3.2.1). Lời gọi tới phương thức ảo từ. Phương thức ảo và tương ứng bội 3.1. Cách định nghĩa phương thức ảo Giả sử A là lớp cơ sở, các lớp B, C, D dẫn