Các hàm ảo thực sự

Một phần của tài liệu Giáo trình lập trình hướng đối tượng (Trang 136)

CHƢƠNG VII : RÀNG BUỘC ĐỘNG VÀ ĐA THỂ

7. Các hàm ảo thực sự

Sẽ là tốt hơn nếu , đã quyết đi ̣nh ta ̣o ra mô ̣t lớp trừu tượng cơ sở , chúng ta có thể (hướ ng dẫn ) ( instruct) chỉ thị cho trình biên dịch ngăn chặn một cách linh động bất cứ người nào sao cho ho ̣ không thể ta ̣o ra bất cứ đối tượng nào của lớp đó . Điều này sẽ cho phép chúng ta tự do hơn trong việc thiết kế lớp cơ s ở vì chúng ta sẽ khơng phải lập kế hoạch cho bất kỳ đối tượng thực sự nào của lớp đó , mà chỉ cần quan tâm tới các dữ liệu và hàm sẽ được sử dụng trong các lớp dẫn xuất . Có một cách để báo cho trình biên dịch biết mơ ̣t lớp là trừu tượng: chúng ta định nghĩa ít nhất một hàm ảo thực sự trong khai báo lớp.

Mô ̣t hàm ảo thực sự là mô ̣t hàm ảo không có thân hàm . Thân của hàm ảo trong lớp cơ sở sẽ được loa ̣i bỏ và ký pháp =0 sẽ được thêm vào khai báo hàm:

7.1 Ví dụ 1

class generic_shape{ // Abstract base class protected:

int x,y; public:

generic_shape(int x_in,int y_in){ x=x_in; y=y_in;} // Constructor

virtual void draw() const =0; //pure virtual function

};

class Line:public generic_shape{ // Line class protected:

int x2,y2; // End coordinates of line public:

Line(int x_in,int y_in,int x2_in,int y2_in):generic_shape(x_in,y_in) {

x2=x2_in; y2=y2_in; }

void draw() const { line(x,y,x2,y2); } // virtual draw function };

//line là mô ̣t hàm thư viê ̣n vẽ mô ̣t đường thẳng lên màn hình class Rectangle:public Line{ // Rectangle class

130

Rectangle(int x_in,int y_in,int x2_in,int

y2_in):Line(x_in,y_in,x2_in,y2_in){}

void draw() const { rectangle(x,y,x2,y2); } // virtual draw };

class Circle:public generic_shape{ // Circle class protected:

int radius; public:

Circle(int x_cen,int y_cen,int r):generic_shape(x_cen,y_cen) {

radius=r; }

void draw() const { circle(x,y, radius); } // virtual draw };

//rectangle và cir cle là các hàm thư viê ̣n vẽ các hình chữ nhâ ̣t và hình tròn lên màn hình int main() { Line Line1(1,1,100,250); Circle Circle1(100,100,20); Rectangle Rectangle1(30,50,250,140); Circle Circle2(300,170,50);

show(Circle1); // show function can take different shapes as argument show(Line1); show(Circle2); show(Rectangle1); return 0; } // hàm vẽ các hình khác nhau void show(generic_shape &shape)

131 { // Which draw function will be called?

shape.draw(); // It 's unknown at compile-time

}

Nếu chúng ta viết mô ̣t lớp cho mô ̣t hình mới bằng cách kế thừa nó từ các lớp đã có chúng ta không cần phải thay đổi hàm show . Hàm này có thể thực hiện chức năng với các lớp mới.

7.2 Ví dụ 2

Trong ví du ̣ này chúng ta sẽ xem xét m ột “Máy trạng thái hữu hạn” (Finite State Machine) FSM.

Chúng ta có các trạng thái: {1, 2, 3} Input: {a, b}, x để thoát

Output: {x, y}

Các trạng thái của FSM được định nghĩa bằng cách sử dụng một cấu trúc lơp . Mỗi trạng thái sẽ được kế thừa từ lớp cơ sở.

// A Finite State Machine with 3 states #include<iostream.h>

// *** Base State (Abstract Class) *** class State{

protected:

State * const next_a, * const next_b; // Pointers to next state char output;

public:

State( State & a, State & b):next_a(&a),next_b(&b){} virtual State* transition(char)=0;

132 // *** State1 ***

class State1:public State{ public:

State1( State & a, State & b):State(a,b){} State* transition(char);

};

// *** State2 ***

class State2:public State{ public:

State2( State & a, State & b):State(a,b){} State* transition(char);

};

/*** State3 ***/

class State3:public State{ public:

State3( State & a, State & b):State(a,b){} State* transition(char);

};

State* State1::transition(char input) {

cout << endl << "Current State: 1"; switch(input){

case 'a': output='y';

cout << endl << "Output: "<< output; cout << endl << "Next State: 1";

return next_a;

case 'b': output='x';

cout << endl << "Output: "<< output; cout << endl << "Next State: 2";

133 default : cout << endl << "Undefined input";

cout << endl << "Next State: Unchanged";

return this;

} }

State* State2::transition(char input) {

cout << endl << "Current State: 2"; switch(input){

case 'a': output='x';

cout << endl << "Output: "<< output; cout << endl << "Next State: 3";

return next_a;

case 'b': output='y';

cout << endl << "Output: "<< output; cout << endl << "Next State: 2";

return next_b;

default : cout << endl << "Undefined input";

cout << endl << "Next State: Unchanged";

return this;

} }

State* State3::transition(char input) {

cout << endl << "Current State: 3"; switch(input){

case 'a': output='y';

cout << endl << "Output: "<< output; cout << endl << "Next State: State1";

return next_a;

134 cout << endl << "Output: "<< output;

cout << endl << "Next State: 2";

return next_b;

default : cout << endl << "Undefined input";

cout << endl << "Next State: Unchanged";

return this;

} }

// *** Finite State Machine ***

// This class has 3 State objects as members class FSM{ State1 s1; State2 s2; State3 s3; State * current; public: FSM():s1(s1,s2),s2(s3,s2),s3(s1,s2),current(&s1) {} void run(); }; void FSM::run() { char in;

cout << endl << "The finite state machine starts ..."; do{

cout << endl << "Give the input value (a or b; x:EXIT) "; cin >> in;

if (in != 'x')

current = current->transition(in); else

135 }while(current);

cout << endl << "The finite state machine stops ..." << endl;; } int main() { FSM machine1; machine1.run(); return 0; }

Hàm transition của mỗi trạng thá i xác đi ̣nh hành vi của FSM . Nó nhận giá trị input như là tham sớ, kiểm tra input sinh ra giá tri ̣ output tùy thuô ̣c vào giá tri ̣ input và trả về đi ̣a chỉ của trạng thái tiếp theo . Hàm chuyển đổi (transition)của trạng thái hiện tại được gọi . Giá trị trả về của hàm này sẽ xác định trạng thái tiếp theo của FSM.

8. Cấu tƣ̉ ảo và hủy tƣ̉ ảo

Khi chúng ta ta ̣o ra mô ̣t đối tượng chúng ta thường là đã biết kiểu của đối tượng đang đươ ̣c ta ̣o ra và chúng ta có thể chỉ đi ̣nh điều này cho trình biên di ̣ch . Vì thế chúng ta không cần có các cấu tử ảo . Cũng như vậy một hàm cấu tử của một đối tượng thiết lập cơ chế ảo của nó (bảng hàm ảo ) trước tiên. Chúng ta khơng nhìn thấ y đoa ̣n mã chương trình này , tất nhiên, cũng như chúng ta khơng nhìn thấy đoạn mã khởi tạo vùng nhớ cho một đối tượng . Các hàm ảo khơng thể thậm chí tồn tại trừ khi hàm cấu tử hòan thành cơng việc của nó vì thế các hàm cấu tử không thể là các hàm ảo.

Hàm hủy tử ảo: Ví dụ:

// non-virtual function used as base class destructor #include <iostream.h>

class Base{ public:

~Base() { cout << "Base destructor" << endl; } // Destructor is not virtual };

class Derived : public Base{ public:

~Derived() { cout << "Derived destructor" << endl; } // Non-virtual };

int main() {

136 Base* pb; // pb can point to objects of Base ans Derived

pb = new Derived; // pb points to an oject of Derived delete pb;

cout << "Program terminates" << endl; return 0;

}

Hãy nhớ lại rằng một đối tượng của lớp dẫn xuất thường chứa dữ liệu từ cả lớp cơ sở và lớp dẫn xuất. Để đảm bảo rằng các dữ liê ̣u này được goodbye mô ̣t cách hoàn hảo có thể cần có những lời go ̣i tới hàm hủy tử cho cả lớp cơ sở và lớp dẫn xuất . Nhưng output của ví dụ trên là:

Base Destructor Program terminates

Trong chương trình này bp là mô ̣t con trỏ của lớp cơ sở (kiểu Base). Vì thế nó có thể trỏ tới các đối tượng thuộc lớp Base và Derived . Trong ví du ̣ trên bp trỏ tới mô ̣t đối tượng thuô ̣c lớp Derived nhưng trong khi xóa con trỏ này chỉ hàm hủy tử của lớp Base là được gọi tới. Vấn đề tương tự cũng đã được bắt gă ̣p với các hàm bình thường trong phần trước (các hàm không là hàm hủy tử ). Nếu như một hàm không phải là hàm ảo chỉ có phiên bản của lớp cơ sở là được gọi tới thậm chí nếu nội dung của con trỏ là một địa chỉ của một đối tươ ̣ng của lớp dẫn xuất . Vì thế trong ví dụ trên hàm hủy tử của lớp Derived sẽ không bao giờ được go ̣i tới . Điều này có thể là mô ̣t tai ho ̣a nếu như hàm này thực hiê ̣n mô ̣t vài công viê ̣c cao thủ nào đó . Để sửa chữa chúng ta chỉ cần làm cho hàm hủy tử này trở thành hàm ảo và thế là mọi thứ sẽ trở lại bình thường.

9. Hàm tốn tử ảo

Như chúng ta đã thấy khái niê ̣m về ràng buô ̣c đô ̣ng có nghĩa là kiểu đô ̣ng (dynamic type) của đối tượng thự c sự sẽ quyết đi ̣nh hàm nào sẽ được go ̣i thực hiê ̣n . Nếu chẳng ha ̣n chúng ta thực hiện lời gọi : p->f(x) trong đó p là mô ̣t con trỏ x là tham số và f là mơ ̣t hàm ảo, thì chính là kiểu của đối tượng mà p trỏ tới sẽ xác đị nh biến thể (variant) nào của hàm f sẽ được gọi thực hiện . Các toán tử cũng là các hàm thành viên nếu như chúng ta có một biểu thức:

a XX b

Trong đó XX là mô ̣t ký hiê ̣u toán tử thì điều này cũng giống như chúng ta có câ u lê ̣nh sau:

a.operatorXX(b);

Nếu như hàm toán tử operatorXX được khai báo là mô ̣t hàm ảo thì chúng ta sẽ thực hiê ̣n lời go ̣i hàm như sau:

(*p) XX (*q);

Trong đó p và q là hai con trỏ và khi đó kiểu của đối tượng mà p trỏ tới sẽ xác định biến thể nào của hàm toán tử operatorXX sẽ được go ̣i tới để thực hiê ̣n . Viê ̣c con trỏ q trỏ tới đối tượng thuô ̣c lớp nào là không quan tro ̣ng , nhưng C++ chỉ cho phép ràng buộc động

137 đối với toán tử đầu tiên . Điều này có thể làm nảy sinh rắc rối nếu chúng ta muốn viết các tốn tử đối xứng với hai tham số có cùng kiểu . Chẳng ha ̣n giả sử chúng ta ḿn xây dựng tốn tử > để so sánh hai đối tượng cùng thuộc kiểu Teacher (có nghĩa là có thể là thuộc lớp Teacher hoă ̣c lớp Principal) trong các ví du ̣ trước. Nếu chúng ta có hai con trỏ t1 và t2:

Teacher *t1, * t2;

Chúng ta muốn rằng có thể thực hiện so sánh bằng câu lệnh sau: if(*t1 > *t2)

….

Có nghĩa là chúng ta muốn thực hiện ràng buộc động đối với hàm tốn tử > sao cho có thể gọi tới các biến thể khác nhau của nó tùy thuộc vào kiểu của các đối tượng mà hai con trỏ t1 và t2 trỏ tới. Ví dụ nếu chúng cùng trỏ tới các đối tượng thuộc lớp Teacher chúng ta có thể so sánh theo hai điều kiê ̣n là tên và số sinh viên quản lý , còn nếu chúng cùng trỏ tới hai đối tượng thuô ̣c lớp Principal chúng ta sẽ so sánh thêm tiêu chí thứ 3 là tên trường. Để có thể thực hiện điều này theo nguyên tắc của ràng buộc động chúng ta sẽ khai báo hàm toán tử > là hàm ảo ở lớp cơ sở (Teacher):

class Teacher{ ..

pubic:

virtual Bool operator > (Teacher & rhs); };

Sau đó trong lớp dẫn xuất chúng ta có khai báo tiếp như sau: class Principal: public Teacher{

.. pubic:

Bool operator > (Principal & rhs); };

Theo nguyên lý thông thường về các hàm ảo chúng ta mong muốn là toán tử > sẽ họat động tốt với các đối tượng th uô ̣c lớp Principal (hay chính xác hơn là các con trỏ trỏ vào các đối tượng thuộc lớp đó ). Tuy nhiên thâ ̣t khơng may đây la ̣i là mô ̣t lỗi , đi ̣nh nghĩa lớp như thế sẽ không thể biên di ̣ch được, thâm chí ngay cả trong trường hợp mà có thể biên dịch được thì chúng ta cũng khơng thể sinh bất cứ đối tượng nào thuộc lớp Principal vì sẽ làm xuất hiện lỗi biên dịch . Lý do là vì trình biên dịch hiểu rằng lớp Principal là một lớp trừu tượng và nguyên nhân là ở tham số trong khai báo của hàm toán tử >. Điều này là vì khai báo hàm toán tử trong lớp Principal không khớp với kiểu tham số trong khai báo của lớp cơ sở Teacher do đó hàm toán tử > của lớp cơ sở Teacher sẽ không được kế thừa hay bi ̣ ẩn đi và điều này có nghĩa là lớp Principal vẫn có một hàm tốn tử > là hàm ảo thực sự do đó nó là lớp ảo.

138 class Principal: public Teacher{

.. pubic:

Bool operator > (Teacher & rhs); };

Bây giờ thì trình biên di ̣ch không kêu ca phàn nàn gì nữa và chúng ta xem xét phần cài đặt hàm:

Bool Principal::operator > (Teacher & rhs){ if(name > rhs.name)

return True;

else if((name == rhs.name) && (numOfStudents > rhs.numOfStudents)) return True;

else if((name == rhs.name) && (numOfStudents == rhs.numOfStudents) && (schoolName > rhs.schoolName))

return True; return False; };

Tuy nhiên ở đây chúng ta la ̣i gă ̣p phải l ỗi biên dịch . Trình biên dịch sẽ kêu ca rằng thành viên schoolName không phải là một thành viên của lớp Teacher . Điều này là chính xác và để khắc phục nó chúng ta cần thực hiện một thao tác chuyển đổi kiểu (casting) cho tham số truyền vào của hàm toán tử >:

Bool Principal::operator > (Teacher & rhs){ Principal & r = Dynamic_cast<Principal&>(rhs); if(name > r.name)

return True;

else if((name == r.name) && (numOfStudents > r.numOfStudents)) return True;

else if((name == r.name) && (numOfStudents == r.numOfStudents) && (schoolName > r.schoolName))

return True; return False; };

Chúng ta cũng thực hiện hồn tồn tương tự với các tốn tử khác hay các đối tượng khác có vai trò tương tự như lớp Principal.

139

10. Bài tập Bài tập 1: Bài tập 1:

Có 3 lớp câu hỏi trắc nghiê ̣m : loại 1 có các trường id , nơ ̣i dung câu hỏi , các lựa chọn A, B, C, D và đáp án (là 1 trong 4 lựa cho ̣n A, B, C, D), loại 2 có các trường id , nơ ̣i dung câu hỏi, hình ảnh, các lựa chọn A , B, C, D và đáp án (là 1 trong 4 lựa cho ̣n A, B, C, D), loại 3 có các trường id, nợi dung câu hỏi, hình ảnh 1, hình ảnh 2, các lựa chọn A, B, C, D và đáp án (là 1 trong 4 lựa cho ̣n A, B, C, D). Đề thi có tổng số m câu hỏi trong đó m1

câu đươ ̣c cho ̣n ngẫu nhiên từ mảng n1 câu hỏi loa ̣i 1, m2 câu được cho ̣n ngẫu nhiên từ mảng n2 câu hỏi loa ̣i 2 và m3 câu được cho ̣n ngẫu nhiên từ mảng n3 câu hỏi loa ̣i 3 (m1 + m2 + m3 == m).

Hãy viết chương trì nh nhâ ̣p vào mơ ̣t mảng gồm có : n1 câu hỏi loa ̣i 1, n2 câu hỏi loại 2, n3 câu hỏi loa ̣i 3, sau đó nhâ ̣p các tham số m 1, m2, m3, m và đưa ra các câu hỏi sẽ có trong đề thi.

Yêu cầu kỹ thuâ ̣t : Chỉ sử dụng một mảng để lưu 3 loại câu hỏi , sử du ̣ng mảng đô ̣ng và kỹ thuâ ̣t đa thể (Polymorphism), các câu hỏi được chọn vào đề thi phải khác nhau.

140

CHƢƠNG VIII: BẢN MẪU (TEMPLATE) 1. Các lớp bản mẫu

Khi viết các chương trình chúng ta luôn có nhu cầu sử du ̣ng các cấu trúc có khả năng lưu trữ và xử lý mô ̣t tâ ̣p các đới tượng nào đó . Các đối tượng này có thể cùng kiểu – khi đó chúng ta có tâ ̣p các đối tượng đồng nhất , hoă ̣c chúng có thể có kiểu khác nhau khi đó ta có các tâ ̣p đối tượng không đồng nhất hay hỗn hợp . Để xây dựng lên các cấu trúc đó chúng ta có thể sử dụng mảng hay các cấu trúc dữ liệu chẳng hạn như danh sách , hàng đợi, hoặc là cây . Mô ̣t lớp có thể được dùng để xây dựng nên các collection object được go ̣i là mô ̣t lớp chứa. Các lớp Stack, Queue hoă ̣c Set đều là các ví du ̣ điển hình về lớp chứa . Vấn đề với các lớp chứa mà chúng ta đã biết n ày là chúng được xây dựng chỉ để chứa các đối tươ ̣ng kiểu cơ bản (int, char *, …). Như vâ ̣y nếu chúng ta muốn xây dựng mô ̣t hàng đợi chẳng ha ̣n để chứa các đối tượng thuô ̣c lớp Person chẳng ha ̣n thì lớp Queue này la ̣i không thể sử du ̣ng được , giải pháp là chúng ta lại xây dựng một lớp mới chẳng hạn là

Person_Queue. Đây là mô ̣t phương pháp không hiê ̣u quả và nó đòi hỏi chúng ta phải xây dựng la ̣i hoàn toàn các cấu trúc mới với các kiểu dữ liê ̣u (lớp) mới.

C++ cung cấp một khả năng cho phép chúng ta không phải lă ̣p la ̣i công viê ̣c ta ̣o mới này bằng cách tạo ra các lớp chung . Mô ̣t bản mẫu sẽ được viết cho lớp , và trình biên dịch sẽ tự động sinh ra các lớp khác nhau cần thiết từ bản mẫu này. Lớp chứa cần sử du ̣ng sẽ chỉ

Một phần của tài liệu Giáo trình lập trình hướng đối tượng (Trang 136)

Tải bản đầy đủ (PDF)

(169 trang)