Bài giảng lập trình hướng đối tượng - Thầy Cường Học viện bưu chính viễn thông TP HCM
Trang 1Chương 9
Tính đa hình
• Con trỏ và Lớp dẫn xuất
• Dẫn nhập các hàm ảo
• Các hàm ảo thuần túy
• Áp dụng đa hình
Trang 2I/ Con trỏ và Lớp dẫn xuất
1/ Khái niệm
Tính đa hình (polymorphism) được hổ trợ bằng hai cách khác nhau trong C++
Cách 1, đa hình được hổ trợ khi biên dịch chương trình (compiler) thông qua việc quá
tải các hàm và toán tử
Cách 2, đa hình được hổ trợ ở thời điểm thực thi chương trình (run-time) thông qua
các hàm ảo Cách này giúp lập trình viên linh động hơn
• Cơ sở của hàm ảo và đa hình khi thực thi chương trình là các con trỏ của lớp dẫn xuất
Chương 3 có khảo sát về con trỏ, một đặc tính mới của con trỏ sẽ được khảo sát
trong chương này Nếu p là một con trỏ tới lớp cơ sở, thì có thể sử dụng p để trỏ tới
bất kỳ lớp nào được suy ra từ lớp cơ sở
Chẳng hạn, có hai lớp cơ sở base và lớp dẫn xuất derived kế thừa base, các phát biểu
sau đều đúng
base *p; // base class pointer
base base_ob; // object of type base
derived derived_ob; // object of type derived
// p can, of course, point to base objects
p = &base_ob; // p points to base object
// p can also point to derived objects without error
p = &derived_ob; // p points to derived object
Một con trỏ của lớp cơ sở có thể trỏ tới bất kỳ lớp dẫn xuất nào của lớp cơ sở mà
không gây ra báo lỗi khác kiểu Song chỉ có thể truy cập được các thành phần mà lớp
dẫn xuất được kế thừa từ lớp cơ sở Bởi vì con trỏ của lớp cơ sở chỉ biết lớp cơ sở mà thôi, nó không biết gì những thành phần được thêm vào bởi lớp dẫn xuất
Trang 3• Một con trỏ của lớp dẫn xuất không thể dùng để truy cập một đối tượng của lớp
cơ sở (Việc sử dụng linh hoạt kiểu có thể dùng để khắc phục hạn chế nói trên, nhưng nó không được khuyến khích sử dụng)
Các phép toán số học trên con trỏ liên quan đến kiểu dữ liệu mà con trỏ đó được
khai báo để trỏ đến Do đó, nếu con trỏ đến một đối tượng lớp dẫn xuất, rồi tăng nội
dung con trỏ lên 1 Điều này không làm cho con trỏ chỉ đến đối tượng mới của lớp
dẫn xuất, mà nó sẽ chỉ đến đối tượng mới của lớp cơ sở
Ví dụ 1.1 Dùng con trỏ của lớp cơ sở để truy cập đến lớp dẫn xuất
// Demonstrate pointer to derived class
base *p; // pointer to base type
base b_ob; // object of base
derived d_ob; // object of derived
// use p to access base object
p = &b_ob;
Trang 4p->setx(10); // access base object
cout << "Base object x: " << p->getx() << '\n';
// use p to access derived object
p = &d_ob; // point to derived object
p->setx(99); // access derived object
// can't use p to set y, so do it directly
d_ob.sety(88);
cout << "Derived object x: " << p->getx() << '\n';
cout << "Derived object y: " << d_ob.gety() << '\n';
Khai báo hàm ảo bắt đầu bằng từ khoá virtual Một lớp có chứa hàm ảo được kế
thừa, lớp dẫn xuất sẽ tái định nghiã hàm ảo đó cho chính mình
• Các hàm ảo triển khai ý tưởng chủ đạo của đa hình là "một giao diện cho nhiều
phương thức " Hàm ảo bên trong một lớp cơ sở định nghiã hình thức giao tiếp đối
với hàm đó
Việc tái định của hàm ảo ở lớp dẫn xuất là thi hành các tác vụ của hàm liên quan đến
chính lớp dẫn xuất đó Nói cách khác, tái định các hàm ảo chính là tạo ra các phương thức cụ thể Hàm ảo tái định ở lớp dẫn xuất không cần sử dụng từ khoá virtual
• Nguyên lý làm việc của đa hình trong khi thực thi chương trình :
Trang 5Hàm ảo được gọi thực thi giống như các hàm thành phần bình thường của lớp Tuy nhiên, khi gọi hàm ảo bằng con trỏ, việc hổ trợ tính đa hình trong khi thực thi chương trình sẽ xảy ra
Khi một con trỏ trỏ đến một lớp dẫn xuất có chứa hàm ảo và hàm ảo này được gọi bằng con trỏ thì trình biên dịch sẽ xác định phiên bản nào của hàm đó sẽ được thực thi Do đó nếu có hai hay nhiều lớp dẫn xuất của một lớp cơ sở nào đó, và chúng đều có chứa hàm ảo, thì con trỏ của lớp cơ sở có thể trỏ đến các đối tượng khác nhau của lớp dẫn xuất nói trên, tức là có thể gọi đến nhiều phiên bản khác nhau của các hàm ảo
• Một lớp có chứa hàm ảo được gọi là lớp đa hình
Trang 62/ Hàm ảo và quá tải hàm
Giải thích kết qủa chương trình ?
Using base version of func() : 10 Using derived1's version of func() : 100 Using derived2's version of func() : 20
Trang 7Việc tái định hàm ảo trong một lớp dẫn xuất có tương tự như quá tải hàm không ?
Câu trả lời là không
Quá tải hàm Hàm ảo
Số lượng đối số Cho phép khác biệt Phải giống nhau
Kiểu dữ liệu của đối số Cho phép khác biệt Phải giống nhau
Hàm thành phần của lớp Không bắt buộc Bắt buộc
Vị trí
Việc tái định hàm ảo trong một lớp dẫn xuất còn được gọi là gán thứ tự ưu tiên cao
hơn cho hàm đó
3/ Các hàm ảo được phân cấp theo thứ tự kế thừa
Nếu lớp dẫn xuất không tái định hàm ảo nào đó thì lớp này sẽ sử dụng phiên bản hàm của lớp cơ sở
Trang 8// This example illustrates how a virtual function can be used to respond to random
// events occurring at run time
Keát quûa chöông trình
Using base version of func() : 10 Using derived1's version of func() : 100 Using base version of func() : 10
Trang 10if( ( j % 2) ) p = &d_ob1; // if odd use d_ob1
else p = &d_ob2; // if even use d_ob2
5/ Sử dụng hàm ảo để triển khai cách thức giao diện
Trang 11void getdim(double &d1, double &d2) {
d1 = dim1;
d2 = dim2;
}
virtual double getarea() {
cout << "You must override this function\n";
Trang 12virtual void shownum() { cout << i << '\n';}
Trang 13distance (double f) { d = f;}
virtual void trav_time() {
// define here
} };
Tạo hàm ảo trav_time() của lớp distance xuất ra khoảng thời gian cần thiết để đi qua khoảng cách này Giả sử rằng đơn vị chiều dài là mile và vận tốc di chuyển là 60 mile/hours
Hãy tạo lớp dẫn xuất metric từ lớp distance, với hàm ảo trav_time() in ra khoảng thời gian cần thiết để đi qua khoảng cách trên, tuy nhiên lúc này đơn vị chiều dài là
km và vận tốc là 100km/giờ
III/ Các hàm ảo thuần túy (pure virtual function)
Một trường hợp khá phổ biến khi lớp cơ sở tự nó không phải là một lớp hoàn chỉnh,
lúc đó hàm ảo được khai báo giữ chỗ chứ không thực hiện công việc cụ thể Lớp cơ sở này chỉ cung cấp một bộ khung gồm các hàm và biến và để dành cho các lớp dẫn xuất các phần định nghĩa còn lại Các lớp dẫn xuất phải tái định tất cả các hàm ảo khai báo trong lớp cơ sở đó Để đảm bảo cho điều này, C++ hổ trợ một công cụ gọi là hàm ảo thuần túy
• Hàm ảo thuần túy không có một định nghiã nào liên quan đến lớp cơ sở Nó chỉ có dạng hàm mà thôi, cách khai báo
virtual <type> func-name(arg-list) = 0 ;
Hàm được gán trị zero với mục đích thông báo cho trình biên dịch biết là không có
gì trong hàm là liên quan đến lớp cơ sở
Khi hàm ảo là một hàm ảo thuần túy, nó buộc các lớp dẫn xuất phải tái định nó, nếu không trình biên dịch sẽ báo lỗi lúc biên dịch chương trình
• Nếu một lớp có chứa hàm ảo thuần túy, lớp đó được gọi là lớp trừu tượng
Lớp trừu tượng được tạo ra với mục đích phải được kế thừa, không được và không thể tạo ra để tồn tại đơn độc
Trang 14Tuy nhiên, có thể tạo ra một con trỏ đến lớp trừu tượng bởi vì điều này cần thiết để
hổ trợ cho đa hình trong khi run-time Điều này cũng cho phép tham chiếu đến một lớp trừu tượng
• Khi một hàm ảo được kế thừa, bản chất ảo của nó cũng được kế thừa
Điều này có nghiã là khi một lớp dẫn xuất kế thừa hàm ảo từ lớp cơ sở, một lớp
dẫn xuất thứ hai lại kế thừa lớp dẫn xuất thứ nhất, hàm ảo có thể được định nghiã
lại ở lớp dẫn xuất thứ hai (cũng như nó được định nghiã lại ở lớp dẫn xuất thứ nhất)
Ví dụ 3.1 Tạo một lớp trừu tượng
Trang 15class triangle : public area {
Ví dụ 3.2 Minh hoạ việc bảo tồn bản chất ảo của hàm khi nó được kế thừa
// Virtual functions retain their virtual nature when inherited
#include <iostream.h>
class base {
public:
virtual void func() {
cout << "Using base version of func()\n";
}
};
Trang 16class derived1 : public base {
// Derived2 inherits derived1
class derived2 : public derived1 {
Kết quả chương trình ?
Điều gì xảy ra khi cả hai lớp dẫn xuất đều không tái định hàm ảo func() ?
Trang 17Bài tập III
1 Hãy tạo ra một đối tượng cho lớp area trong ví dụ 3.1 chương 9, điều gì sẽ xảy ra
?
2 Trong ví dụ 3.2 chương 9, thử xoá phần tái định hàm ảo func() của lớp derived2, rồi cho truy cập d_ob2 Kết qủa của chương trình thế nào ?
3 Tại sao không thể tạo ra một đối tượng thuộc lớp trừu tượng ?
IV/ Aùp dụng đa hình
Tại sao cần sử dụng đa hình ?
1/ Đa hình rất quan trọng vì nó đơn giản hoá các hệ thống phức tạp
• Đa hình là một quá trình áp dụng một giao diện cho hai hay nhiều trường hợp
tương tự nhau (nhưng khác biệt về mặt kỹ thuật), nó triển khai tư tưởng "một
giao diện cho nhiều phương thức"
Một giao diện hay và đơn giản được dùng để truy cập đến một số các hoạt động có liên hệ với nhau nhưng khác nhau, và làm mất đi sự phức tạp giả tạo của hệ thống
các hoạt động này
• Đa hình làm cho mối quan hệ luận lý giữa các hoạt động tương tự nhau được trở nên rõ ràng hơn, đo đó nó giúp cho lập trình viên dễ dàng hơn trong việc đọc hiểu và bảo trì chương trình
Một khi các hoạt động có liên quan với nhau được truy cập bằng duy nhất một giao diện, giúp sẽ nhớ hơn Giao diện đồ hoạ trên hệ điều hành Windows hoặc Macintosh là một điển hình
2/ Khái niệm về liên kết
Liên kết (binding) liên quan đến OOP và ngôn ngữ C++ Có hai khái niệm :
Trang 18• Liên kết sớm (early binding) gắn liền với những biến cố có thể xác định ở thời
điểm biên dịch chương trình Đặc biệt, liên kết sớm liên quan đến các gọi hàm được xử lý trong lúc biên dịch bao gồm :
- Các hàm thông thường
- Các hàm được quá tải
- Các hàm thành phần không phải là hàm ảo
- Các hàm friend
Tất cả các thông tin về địa chỉ cần thiết cho việc gọi các hàm trên được xác định rõ ràng trong lúc biên dịch
Ưu điểm : gọi các hàm liên kết sớm là kiểu gọi hàm nhanh nhất
Nhược điểm : thiếu tính linh hoạt
• Liên kết muộn (late binding) gắn liền với những biến cố xuất hiện trong lúc
thực thi chương trình (run-time)
Khi gọi các hàm liên kết muộn, điạ chỉ của hàm được gọi chỉ biết được khi run-time
Trong C++, hàm ảo là một đối tượng liên kết muộn Chỉ khi run-time, hàm ảo được truy cập bằng con trỏ của lớp cơ sở, chương trình mới xác định kiểu của đối tượng bị trỏ và biết được phiên bản nào của hàm ảo được thực thi
Ưu điểm : tính linh hoạt của nó ở thời gian run-time, điều này giúp cho chương trình gọn gàng vì không có những đoạn chương trình xử lý các biến cố ngẫu nhiên trong khi thực thi
Nhược điểm : chậm hơn so với liên kết sớm, do phải qua nhiều giai đoạn trung gian kèm theo việc gọi thực thi một hàm liên kết muộn
Mỗi loại liên kết đều có những ưu khuyết điểm riêng của nó, nên phải cân nhắc để quyết định tình huống thích hợp để sử dụng hai loại liên kết nói trên
Ví dụ 4.1 Minh họa tư tưởng "một giao diện cho nhiều phương thức"
Trang 19// Demonstrate virtual functons
list *head; // pointer to start of list
list *tail; // pointer to end of list
list *next; // pointer to next item
int num; // value to be stored
list() { head = tail = next = NULL; }
virtual void store(int i) = 0;
virtual int retrieve() = 0;
};
// Create a queue type list
class queue : public list {
Trang 20if(tail) tail->next = item;
// Create a stack type list
class stack : public list {
Trang 21cout << "Allocation error.\n";
exit(1);
}
item->num = i;
// put on front of list for stack-like operation
if(head) item->next = head;
Trang 22Ví dụ 4.2 Dùng đa hình để xử lý các biến cố ngẫu nhiên
Sử dụng các khai báo và định nghiã lớp ở ví dụ 4.1
Trang 23char ch;
int i;
for(i=0; i<10; i++) {
cout << "Stack or Queue? (S/Q): ";
• Trong HĐH Windows và OS2, thông qua giao diện người dùng giao tiếp với một
chương trình bằng cách gởi đến chương trình các messages Những message này
được phát sinh một cách ngẫu nhiên và chương trình của bạn phải đáp ứng các message này mỗi khi nhận được nó Do đó, đa hình giúp thực thi chương trình rất hữu hiệu cho các chương trình được viết để sử dụng trong các HĐH nói trên
Ví dụ 4.3 Một chương trình đơn giản nhất trên Windows, xuất ra màn hình
Trang 24một khung cửa sổ chứa nội dung " Hello, Windows 98 ! "
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ;
hwnd = CreateWindow (szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
Trang 25CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
////LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM
wParam, LPARAM lParam)
Trang 26DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
danh sách Đặt tên cho lớp này là sorted thừa kế lớp cơ sở list
Lớp sorted chứa hai hàm chung là
- void store(int i) có chức năng thêm phần tử mới vào danh sách sao cho chúng có thứ tự tăng dần
- và int retrieved() có chức năng hiển thị các phần tử trong danh sách
2 Hãy viết một chương trình có áp dụng tính đa hình
Bài tập chương 9
Trang 271 Xét đoạn chương trình sau đây Tìm lỗi và giải thích tại sao ?
2 Trình bày sự khác nhau giữa hàm ảo và quá tải hàm
3 Viết chương trình bổ sung vào ví dụ 4.1 chương 9, bằng cách quá tải hai toán tử + và toán tử vào lớp dẫn xuất stack và queue
Toán tử + thêm một phần tử vào danh sách
stack operator + (int i) ;
queue operator + (int i) ;
và toán tử lấy một phần tử ra khỏi danh sách
int operator (int unused) ; // cho cả stack và queue
4 Hãy sửa đổi một số ví dụ về quá tải hàm trong các chương trước, sao cho có thể chuyển đổi các hàm được quá tải thành các hàm ảo ?