You are on page 1of 24

CHƯƠNG 5.

ĐA HÌNH
Mục đích của chương
Nội dung chương này trình bày các kiến thức liên quan đến tính đa hình trong lập
trình hướng đối tượng C++
 Khái niệm về tính đa hình.
 Hàm ảo, phương thức thuần ảo trong lớp.
 Lớp trừu tượng.

Giới thiệu
Đa hình là công cụ mạnh của C++ có ý nghĩa hết sức quan trọng trong lập trình
hướng đối tượng bởi nó làm tăng hiệu quả của chương trình. Theo nghĩa tổng quát, đa
hình là khả năng tồn tại ở nhiều hình thức. Trong hướng đối tượng, đa hình đi kèm với
quan hệ kế thừa và có hai đặc điểm sau: thứ nhất, các đối tượng thuộc các lớp dẫn xuất
khác nhau có thể được đối xử như nhau, như thể chúng là các đối tượng thuộc lớp cơ sở,
chẳng hạn có thể gửi cùng một thông điệp tới đối tượng; thứ hai, khi nhận được cùng một
thông điệp đó, các đối tượng thuộc các lớp dẫn xuất khác nhau hiểu nó theo những cách
khác nhau. Đa hình và hàm ảo cho phép tổ chức quản lý các đối tượng khác nhau theo
cùng một lược đồ. Chương này sẽ trình bầy cách sử dụng đa hình để xây dựng chương
trình quản lý nhiều đối tượng khác nhau theo một lược đồ thống nhất.
Điểm Đường Thẳng Hình Tròn Hình Vuông

Vẽ

Hình 5.1. Minh họa tính đa hình

Kỹ thuật đa hình trong các ngôn ngữ lập trình hướng đối tượng giúp các lập trình
viên gia tăng khả năng tái sử dụng những đoạn mã nguồn được viết một cách tổng quát
và có thể thay đổi cách ứng xử một cách linh hoạt tùy theo loại đối tượng. Ví dụ trong
hình 5.1 mô tả các lớp Điểm, Đường thẳng, Hình tròn, Hình vuông. Mỗi lớp đều có
chung phương thức “Vẽ”, tuy nhiên khi thực thi cùng một phương thức nhưng với các đối
tượng ở các lớp khác nhau sẽ cho ra kết quả khác nhau.
Đa hình được hiểu theo hai cách như sau:
- Đa hình biên dịch (compile – time polymorphism): Các hành động cùng tên thực
thi trên cùng một đối tượng vẫn có thể thực hiện khác nhau (có kết quả khác nhau)
tùy thuộc vào ngữ cảnh dựa trên kỹ thuật nạp chồng phương thức (overloading) và
cơ chế liên kết sớm (Early Binding).
- Đa hình thực thi (run-time polymorphism): Các đối tượng khác nhau khi nhận
chung một yêu cầu vẫn có thể có những ứng xử khác nhau dựa trên kỹ thuật sử
dụng hàm ảo, ghi đè phương thức (overriding) và cơ chế liên kết muộn (Late
Binding). Nói cách khác đây là một cơ chế cho phép gửi một loại thông điệp đến
nhiều đối tượng khác nhau mà mỗi đối tượng lại có cách xử lý riêng theo ngữ cảnh
tương ứng của chúng.
Trong C++, chúng ta sử dụng nạp chồng phương thức (Method Overloading) và
ghi đè phương thức (Method Overriding) để thể hiện tính đa hình.
- Nạp chồng phương thức (Method Overloading): một lớp có nhiều phương thức cùng
tên nhưng khác nhau về số lượng tham số hoặc kiểu dữ liệu của tham số. Khi gọi
phương thức cùng tên đó, trong quá trình biên dịch, trình biên dịch sẽ quyết định
phương thức nào (trong số các phương thức cùng tên) sẽ được gọi dựa trên số
lượng tham số và kiểu dữ liệu của tham số truyển vào phương thức.
- Ghi đè phương thức (Method Overriding): là hai phương thức cùng tên, cùng tham số,
cùng kiểu trả về nhưng nằm ở hai lớp khác nhau trong mối quan hệ kế thừa.
Phương thức cùng tên ở lớp dẫn xuất được gọi là ghi đè lại phương thức của lớp
cơ sở. Khi thực thi, đối tượng lớp dẫn xuất muốn gọi đến hàm bị ghi đè của lớp cơ
sở phải dùng toán tử phạm vi, con trỏ lớp cơ sở trỏ đến đối tượng lớp dẫn xuất chỉ
có thể gọi đến phương thức trùng ở tên lớp cơ sở đã bị ghi đè, muốn gọi đến
phương thức ở lớp dẫn xuất phải dùng định nghĩa hàm ảo.
Trong chương này chúng ta tập trung tìm hiểu về hàm ảo để thấy được nó cung
cấp tính đa hình thực thi. Nói cách khác, hàm ảo thực hiện theo nhiều phương thức khác
nhau với cùng một hành vi giao tiếp. Con trỏ đóng vai trò quan trọng trong việc gọi hàm
ảo và thực hiện tính đa hình thực thi.
5.1 Hàm ảo
5.1.1 Đặt vấn đề
Tại sao chúng ta cần tạo hàm ảo. Lý do vì trong chương trình có những đối tượng
khác nhau ở những lớp khác nhau nhưng ta muốn lưu trữ chúng vào cùng một mảng, thực
hiện một phép toán và hàm chung cho các đối tượng này. Xét ví dụ sau:
Ví dụ 5.1: Xây dựng chương trình mô tả sơ đồ hình 5.2. Lớp Tam giác, Hình tròn, Hình
chữ nhật kế thừa từ lớp Hình. Tạo một mảng để quản lý danh sách các đối tượng khác
nhau trong các lớp Tam giác, Hình tròn, Hình chữ nhật. Nhập và hiển thị thông tin các
đối tượng trên danh sách đã cho.
Hinh

Các thành phần

nhap()
hienthi()

Tamgiac Hìnhtron Chunhat

Các thành phần Các thành phần Các thành phần

nhap() nhap() nhap()


hienthi() hienthi() hienthi()

Hinh 5.2. Sơ đồ thể hiện mối quan hệ kế thừa của các lớp hình.

#include<iostream.h>
class Hinh
{
private:
int mau;
public:
void nhap()
{
cout<<"Nhap mau : "; cin>>mau;
}
void hienthi()
{
cout<<"\n Mau : "<<mau;
}
};
class Tamgiac: public Hinh
{
private:
float a,b,c;
public:
void nhap()
{
Hinh::nhap();
cout<<"\n Nhap canh a : "; cin>>a;
cout<<"\n Nhap canh b : "; cin>>b;
cout<<"\n Nhap canh c : "; cin>>c;
}
void hienthi()
{
Hinh::hienthi();
cout<<"\n Canh a : "<<a;
cout<<"\n Canh b : "<<b;
cout<<"\n Canh c : "<<c;
}
};
class Hinhtron: public Hinh
{
private:
float r;
public:
void nhap()
{
Hinh::nhap();
cout<<"\n Nhap ban kinh r : "; cin>>r;
}
void hienthi()
{
Hinh::hienthi();
cout<<"\n Ban kinh r : "<<r;
}
};
class Chunhat: public Hinh
{
private:
float a,b;
public:
void nhap()
{
Hinh::nhap();
cout<<"\n Nhap canh a : "; cin>>a;
cout<<"\n Nhap canh b : "; cin>>b;
}
void hienthi()
{
Hinh::hienthi();
cout<<"\n Canh a : "<<a;
cout<<"\n Canh b : "<<b;
}
};
main()
{
Tamgiac abc;
Hinhtron c;
Chunhat hv;
Hinh *h[3];
h[0]= &abc;
h[1] = &c;
h[2]= &hv
for(int i=0;i<=2;i++)
h[i]->nhap();
for(int i=0;i<=2;i++)
h[i]->hienthi();
}
Kết quả chạy chương trình như sau:

Yêu cầu bài toán tạo mảng chứa danh sách các đối tượng khác nhau. Do đó
chương trình có khai báo mảng con trỏ lớp cơ sở *h[3]. Mỗi phần tử trên mảng là một
con trỏ lớp cơ sở để quản lý một đối tượng lớp dẫn xuất (Hinhtron, Tamgiac, Chunhat)
bằng các câu lệnh:
Hinh *h[3];
h[0]= &abc;
h[1] = &c;
h[2]= &hv
Trong cả ba lớp dẫn xuất đều sử dụng kỹ thuật ghi đè (overriding) lại hai phương
thức nhap()và hienthi() của lớp Hinh. Chương trình sử dụng các câu lệnh sau để
nhập xuất thông tin cho các đối tượng:
for(int i=0;i<=2;i++)
h[i]->nhap();
for(int i=0;i<=2;i++)
h[i]->hienthi();
Đầu ra mong muốn là các con trỏ h[i] khi quản lý các đối tượng lớp dẫn xuất sẽ
truy nhập và gọi đến các hàm nhap() và hienthi() ở lớp dẫn xuất đã được ghi đè. Tức là:
- Con trỏ h[0] chứa địa chỉ đối tượng lớp Tamgiac thì hàm nhap() và hienthi() lớp
Tamgiac được gọi.
- Con trỏ h[1] chứa địa chỉ đối tượng lớp Hinhtron thì hàm nhap() và hienthi() lớp
Hinhtron được gọi.
- Con trỏ h[2] chứa địa chỉ đối tượng lớp Chunhat thì hàm nhap() và hienthi() lớp
Chunhat được gọi
Mục đích nhằm in dữ liệu theo đúng yêu cầu. Tuy nhiên, khi biên dịch con trỏ h[i]
không truy nhập đến hàm nhap() và hienthi() của các lớp dẫn xuất. Bởi trình biên dịch chỉ
quan tâm đến kiểu của con trỏ h[i] chứ không quan tâm đến đối tượng mà con trỏ đang
trỏ tới. Dẫn đến hàm nhap() và hienthi() của lớp Hinh được gọi chứ không phải hàm của
đối tượng mà con trỏ đang trỏ tới (của lớp Tamgiac, Hinhtron, Chunhat) được gọi. Kết
quả chạy chương trình chỉ cho phép nhập và in được dữ liệu màu. Các câu lệnh trong
main() thực thi như hình 5.3 dưới đây.
ptr[0]
Lớp Hình

&abc
ptr[0] - > hienthi() hienthi(){...}

ptr[1]

&c

ptr[1] - > hienthi()

ptr[2] Hình tam giác

hienthi()
&hv {...}
ptr[2] - > hienthi()

Hình 5.3. Hoạt động của con trỏ khi không cài đặt hàm ảo

Như đã biết sự kế thừa trong C++ cho phép có sự tương ứng giữa lớp cơ sở và các lớp
dẫn xuất trong sơ đồ kế thừa:  
- Một con trỏ có kiểu lớp cơ sở luôn có thể trỏ đến địa chỉ của một đối tượng của
lớp dẫn xuất. 
- Tuy nhiên, khi thực hiện lời gọi một phương thức của lớp, trình biên dịch sẽ quan
tâm đến kiểu của con trỏ chứ không phải đối tượng mà con trỏ đang trỏ tới:
phương thức của lớp mà con trỏ có kiểu được gọi chứ không phải phương thức của
đối tượng mà con trỏ đang trỏ tới được gọi. 
Giải pháp xử lý vấn đề ở ví dụ trên là xây dựng các hàm ảo sử dụng cơ chế liên kết
chậm hay kết nối động nhằm mục đích giúp con trỏ lớp cơ sở gọi đúng hàm ở lớp dẫn
xuất cần gọi.
5.1.2 Cách định nghĩa hàm ảo
Hàm ảo cũng giống mọi thành viên khác của lớp. Trên thực tế, hàm ảo là một hàm
thành viên khai báo trong lớp cơ sở và được định nghĩa lại (ghi đè) trong các lớp dẫn
xuất.
Khai báo hàm ảo bằng cách thêm từ khóa virtual ở phía trước. Khi đó các phương
thức có cùng kiểu, cùng tên và cùng danh sách tham số với phương thức này trong các
lớp dẫn xuất cũng là hàm ảo.
Cú pháp:
virtual Kiểu_dữ_liệu Tên_phương_thức (Danh sách tham
số)
{
}
Ví dụ 5.2. Cài đặt lại bài toán ở ví dụ 5.1. Định nghĩa phương thức nhap() và hienthi() là
hàm ảo.
class Hinh
{
private:
char mau[30];
public:
virtual void nhap()
{
cin.ignore();
cout<<"Nhap mau : "; cin.getline(mau,30);
}
virtual void hienthi()
{
cout<<"\n\n Mau : "<<mau;
}
};
class Tamgiac: public Hinh
{
private:
float a,b,c;
public:
void nhap()
{
Hinh::nhap();
cout<<"\nNhap canh a : "; cin>>a;
cout<<"\nNhap canh b : "; cin>>b;
cout<<"\nNhap canh c : "; cin>>c;
}
void hienthi()
{
Hinh::hienthi();
cout<<"\nCanh a : "<<a;
cout<<"\nCanh b : "<<b;
cout<<"\nCanh c : "<<c;
}
};
class Hinhtron: public Hinh
{
private:
float r;
public:
void nhap()
{
Hinh::nhap();
cout<<"\nNhap ban kinh r : "; cin>>r;
}
void hienthi()
{
Hinh::hienthi(); cout<<"\n Ban kinh r : "<<r;
}
};
class Chunhat: public Hinh
{
private:
float d,r;
public:
void nhap()
{
Hinh::nhap();
cout<<"\nNhap chieu dai : "; cin>>d;
cout<<"\nNhap chieu rong : "; cin>>r;
}
void hienthi()
{
Hinh::hienthi();
cout<<"\n Canh dai : "<<d;
cout<<"\n Canh rong : "<<r;
}
};
main()
{
Hinh *h[3];
h[0]= new Tamgiac;
h[1] = new Hinhtron;
h[2]= new Chunhat;
for(int i=0;i<=2;i++) h[i]->nhap();
for(int i=0;i<=2;i++) h[i]->hienthi();
}
Kết quả chạy chương trình như sau :
Các câu lệnh trong main() ở ví dụ trên thực hiện như hình 5.4 và kết quả hiển thị
như mong muốn.

ptr[0]
Lớp Hinh

&abc
ptr[0] - > hienthi() hienthi(){...}

ptr[1]

&c
ptr[1] - > hienthi()

ptr[1] Hình chữ nhật

&hv hienthi(){...}
ptr[2] - > hienthi()
Hình 5.4. Hoạt động của con trỏ khi cài đặt hàm ảo

Ở ví dụ trên con trỏ lớp Hinh đã lựa chọn đúng hàm để thực thi dựa trên địa chỉ
của đối tượng được lưu trữ trong nó. Khả năng chọn đúng phiên bản tuỳ theo đối tượng
để thực hiện thông qua con trỏ đến lớp cơ sở được gọi là tính đa hình (polymorphisms).
Các lưu ý khi sử dụng hàm ảo:
Hàm ảo sẽ cung cấp giải pháp (tại thời điểm thực thi).
- Tham chiếu đến kiểu thực sự của đối tượng.
- Sự kết nối xảy ra tại thời điểm thực thi chính là kết nối động.
- Làm giảm tốc độ thực thi của chương trình
- Hàm ảo chỉ hoạt động thông qua con trỏ.
- Hàm ảo chỉ hoạt động nếu các hàm ở lớp cơ sở và lớp con có nghi thức giao tiếp
giống hệt nhau (cùng tên, cùng kiểu, cùng danh sách tham số)
- Nếu ở lớp con không định nghĩa lại hàm ảo thì sẽ gọi phương thức ở lớp cơ sở
(gần nhất có định nghĩa).
5.1.3 Thực thi hàm ảo
Để có thể tìm hiểu chi tiết về vấn đề này trước tiên phải đi sâu tìm hiểu về hai khái
niệm thường được dùng trong kỹ thuật lập trình là: cơ chế kết nối sớm (Early Binding)
hay kết nối tĩnh (Static binding) và cơ chế kết nối muộn (Late Binding) hay kết nối động
(Dynamic binding).
 Kết nối sớm
Khi làm việc với các hàm, hầu hết các ngôn ngữ như C, Pascal,… đều sử dụng cơ
chế kết nối sớm (Early Binding). Trong cơ chế này, tên hàm cụ thể đều gắn liền với một
địa chỉ tương ứng trong bộ nhớ (địa chỉ này được xác định trong quá trình biên dịch).
Trong quá trình liên kết tên hàm sẽ được thay thế bởi các địa chỉ tương ứng. Khi một hàm
nào đó được gọi chương trình sẽ chuyển quyền điều khiển đến địa chỉ tương ứng đã được
xác định trước đó và chỉ một hàm duy nhất sẽ được gọi. Trình biên dịch sẽ biên dịch tất
cả các câu lệnh ở hàm main() cũng như tất cả các hàm tự do và hàm của lớp sang mã máy
trước khi chương trình được thực hiện. Kết nối sớm (Early Binding) đòi hỏi người lập
trình cần phải dự đoán trước hàm nào của đối tượng nào được gọi đến. Các hàm thông
thường, các hàm nạp chồng, hàm thành viên, hàm bạn sử dụng cơ chế kết nối sớm khi
biên dịch. Ưu điểm của có chế này là hiệu quả trong việc gọi hàm. Đây là cách gọi hàm
nhanh nhất. Tuy nhiên nhược điểm của nó là kém linh hoạt, kém mềm dẻo.
Quy tắc gọi phương thức tĩnh:
Lời gọi tới phương thức tĩnh bao giờ cũng xác định rõ phương thức nào (trong số
các phương thức trùng tên của các lớp có quan hệ kế thừa) ñược gọi. Nếu lời gọi xuất
phát từ một đối tượng của lớp nào, thì phương thức của lớp đó sẽ được gọi. Nếu lời gọi
xuất phát từ một con trỏ kiểu lớp nào, thì phương thứ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.
 Kết nối muộn
Kết nối muộn đề cập tới các sự kiện xảy ra tại thời điểm thực thi (thực hiện).
Trong việc gọi hàm, các thông tin địa chỉ chính xác chỉ được biết đến trong lúc thi hành
(run- time). Quá trình này không chỉ có duy nhất một hàm sẽ được thực hiện thông qua
phép gọi hàm mà có thể có nhiều hàm khác nhau sẽ được thực hiện tùy theo từng điều
kiện cụ thể, đây là một cách làm việc mới với hàm trong một số ngôn ngữ lập trình C++,
Java,… Khi biên dịch hàm ảo sử dụng cơ chế kết nối muộn. Trình biên dịch không biên
dịch các hàm ảo khi biên dịch chương trình sang mã máy, mà sẽ biên dịch hàm ảo của
từng lớp kế thừa mỗi khi đối tượng của lớp đó gọi hàm ảo của mình trong khi chương
trình đang thực thi. Kết nối muộn giúp tăng độ mềm dẻo khi lập trình. Tuy nhiên nhược
điểm của quá trình này là việc gọi hàm lâu hơn kết nối sớm.
Quy tắc gọi hàm ảo:
Hàm ảo khác phương thức tĩnh khi được gọi từ một con trỏ. Lời gọi tới hàm ảo từ
một con trỏ chưa cho biết rõ phương thức nào (trong số các hàm ảo trùng tên của các lớp
có quan hệ kế thừa) 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ương thức của lớp đó sẽ
được gọi.
5.2 Hàm thuần ảo (pure virtual functions)
Thông thường hàm ảo trong lớp cơ sở không thực hiện các hoạt động hữu ích.
Nguyên nhân do các lớp cơ sở thường là lớp chung nhất và không hoàn thiện. Nó cung
cấp các biến, hàm cơ bản, còn các biến và hàm còn lại sẽ được định nghĩa trong các lớp
dẫn xuất, nơi chứa các hành động hữu ích thường được thực hiện. Vì không có các hành
động hữu ích được thực hiện trong hàm ảo của lớp cơ sở, nên các phương thức này cần
được viết lại (ghi đè) trong các lớp dẫn xuất. Để thuận tiện, C++ cho phép khai báo hàm
ảo thuần túy trong lớp cơ sở. Hàm thuần ảo là một hàm ảo mà nội dung của nó không có
gì. Cách thức khai báo như sau:

virtual Kiểu_dữ_liệu Tên_phương_thức (Danh sách tham số) = 0;


Sự khác biệt giữa hàm ảo và hàm thuần ảo là hàm thuần ảo luôn đặt bằng 0. Giá trị
0 truyền đạt đến trình biên dịch rằng không có thân hàm của hàm này trong lớp cơ sở.
Khi hàm thuần ảo được khai báo ở lớp cơ sở thì tất cả các lớp dẫn xuất đều phải định
nghĩa lại (ghi đè) hàm này.
Ví dụ 5.3: Cho các lớp như sau:
class NhanVien
{
public:
virtual long int luong()=0; // hàm thuần ảo
…………
}
};
class NVSX: public NhanVien{
int sosp;
public:
……..
long int luong (){
return sosp*2000000;
………..
}
};
class NVKD: public NhanVien{
int sodonhang;
public:
……………
long int luong ()
{
return sodonhang*3000000;
}
……………
}
Lớp NhanVien có sử dụng hàm thuần ảo luong(). Các lớp dẫn xuất NVXS, NVKD
bắt buộc phải định nghĩa lại hàm này.
5.3 Lớp trừu tượng (Asbtract class)
Lớp chứa ít nhất một hàm ảo thuần túy được gọi là lớp trừu tượng (asbtract class).
Một lớp cơ sở trừu tượng là một lớp chỉ được dùng làm cơ sở, định nghĩa một số khái
niệm tổng quát, chung cho các lớp khác. Lớp trừu tượng tồn tại vì mục đích sử dụng để
các lớp khác kế thừa từ nó. Lớp này không phục vụ bất kỳ mục đích riêng hữu ích nào
đối với chính nó bởi vì về mặt ngữ nghĩa nó chưa hoàn thiện đầy đủ theo nghĩa hàm ảo
thuần túy không chứa thân hàm. Đó là nguyên nhân dẫn đến không được phép tạo một
đối tượng thực sự cho lớp trừu tượng, tuy nhiên có thể tạo con trỏ của lớp này.
Ví dụ 5.4:
class NhanVien
{
public:
virtual void nhap() = 0 ;
virtual void xuat() = 0 ;
};
class NVSX: public NhanVien{};
class NVKD: public NhanVien{};
main()
{
NhanVien X,Y; //Sai, không được tạo đối tượng của lớp A
NhanVien *Z, *T[30];
//Đúng, được tạo con trỏ đối tượng của lớp A
}
#include<iostream.h>
class C;
class A
{
public:
int a;
A(int a){ A::a=a;}
virtual void print(C&);
};
class B: public A
{
public:
int b;
B(int i, int j) : A(i)
{
b=j;
}
virtual void print(C&);
};
class C
{
friend void A ::print(C&);
friend void B ::print(C&);
int a,b,c;
public:
C(int i, int j , int k)
{
a=i; b=j; c= k;

}
};
void A::print(C& t)
{
cout<<"\n Thanh phan a cua A la " <<a;
cout<<"\n Thanh phan c cua C la " <<t.c;
}
void B::print(C& t)
{
cout<<"\n Thanh phan b cua B la " <<b;
cout<<"\n Thanh phan c cua C la " <<t.c;
}
void Print(A& m, C & t)
{

m.print(t);
}
main()
{
A a(10);
B b(10,20);
C c(10,20,30);
Print(a,c);
Print(b,c);
}
Trong ví dụ này có thể nhận thấy hai hàm print() của hai lớp A và B được khai báo
như một hàm bạn của lớp C và do đó có thể truy nhập không hạn chế đến các thành phần
của lớp C. Thêm vào đó hai hàm này lại là hai hàm ảo được khai báo từ khóa virtual vì
vậy có thể được gọi một cách chính xác tùy theo đối tượng được truyền như tham chiếu
cho hàm Print().
5.4.2 Toán tử ảo
Toán tử thực chất cũng là một hàm nên chúng ta có thể tạo ra các toán tử ảo trong
một lớp. Tuy nhiên do đa năng hóa khi tạo một toán tử cần chú ý đến các kiểu của các
toán hạng phải sử dụng kiểu của lớp cơ sở gốc có toán tử ảo.
Ví dụ 5.6: Đa năng hóa toán tử với hàm toán tử là hàm ảo.
#include <iostream.h>
class A
{
protected: int X1;
public:
A(int I)
{
X1=I;
}
virtual A& operator + (A& T);
virtual A& operator = (A& T);
virtual int GetA()
{
return X1;
}
virtual int GetB()
{
return 0;
}
virtual int GetC()
{
return 0;
}
void Print(char *St)
{
cout<<St<<":X1="<<X1<<endl;
}
};
class B : public A
{
protected:
int X2;
public:
B(int I,int J):A(I)
{
X2=J;
}
virtual A& operator + (A& T);
virtual A& operator = (A& T);
virtual int GetB()
{
return X2;
}
void Print(char *St)
{
cout<<St<<":X1="<<X1<<",X2="<<X2<<endl;
}
};
class C : public B
{
protected:
int X3;
public:
C(int I,int J,int K):B(I,J)
{
X3=K;
}
virtual A& operator + (A& T);
virtual A& operator = (A& T);
virtual int GetC()
{
return X3;
}
void Print(char *St)
{
cout<<St<<":X1="<<X1<<",X2="<<X2<<",X3="<<X3<<endl;
}
};
A& A::operator + (A& T)
{
X1+=T.GetA();
return *this;
}
A& A::operator = (A& T)
{
X1=T.GetA();
return *this;
}
A& B::operator + (A& T)
{
X1+=T.GetA();
X2+=T.GetB();
return *this;
}
A& B::operator = (A& T)
{
X1=T.GetA();
X2=T.GetB();
return *this;
}
A& C::operator + (A& T)
{
X1+=T.GetA();
X2+=T.GetB();
X3+=T.GetC();
return *this;
}
A& C::operator = (A& T)
{
X1=T.GetA();
X2=T.GetB();
X3=T.GetC();
return *this;
}
void AddObject(A& T1,A& T2)
{
T1=T1+T2;
}
int main()
{
A a(10);
B b(10,20);
C c(10,20,30);
a.Print("a");
b.Print("b");
c.Print("c");
AddObject(a,b);
a.Print("a");
AddObject(b,c);
b.Print("b");
AddObject(c,a);
c.Print("c");
a=b+c;
a.Print("a");
c=c+a;
c.Print("c");
return 0;
}

5.4.3 Hàm hủy bỏ ảo


Khi một đối tượng thuộc lớp có hàm ảo, để thực hiện cơ chế kết nối động, trình
biên dịch sẽ tạo thêm một con trỏ vptr như một thành viên của lớp, con trỏ này có nhiệm
vụ quản lý địa chỉ của hàm ảo. Một lớp chỉ có một bảng hàm ảo, trong khi đó có thể có
nhiều đối tượng thuộc lớp, nên khi một đối tượng khác thuộc cùng lớp tạo ra thì con trỏ
vptr vẫn còn tồn tại. Chính vì vậy bảng hàm ảo phải được tạo ra trước khi gọi thực hiện
hàm tạo, nên hàm tạo không thể là hàm ảo. Ngược lại do một lớp chỉ có một bảng hàm ảo
nên khi một đối tượng thuộc lớp bị hủy bỏ, bảng hàm ảo vẫn còn đó, và con trỏ vptr vẫn
còn đó. Hơn nữa, hàm hủy được gọi thực hiện trước khi vùng nhớ dành cho đối tượng bị
thu hồi, do đó hàm hủy có thể là hàm ảo.
Ví dụ 5.7:
#include <iostream.h>
class Base
{
public:
virtual ~Base()
{
cout<<"~Base"<<endl;
}
};
class Derived:public Base
{
public:
virtual ~Derived()
{
cout<<"~Derived"<<endl;
}
};
int main()
{
Base *B;
B = new Derived;
delete B;
return 0;
}
Nếu hàm hủy không là hàm ảo thì khi giải phóng đối tượng B chỉ có hàm hủy của
lớp cơ sở được gọi, nhưng khi hàm hủy là hàm ảo thì khi giải phóng đối tượng B hàm
hủy của lớp dẫn xuất được gọi thực hiện rồi đến hàm hủy của lớp cơ sở.
TÓM TẮT NỘI DUNG
Chương 5 đề cập đến tính đa hình trong C++. Nội dung chương đã trình bày khái
niệm về hàm ảo, hàm thuần ảo, lớp trừu tượng, các thành viên ảo trong một lớp. Đồng
thời đã hướng dẫn chi tiết cách sử dụng hàm ảo, hàm thuần ảo kết hợp con trỏ để cài đặt
thể hiện được tính đa hình từ đó giúp tổng quát hóa bài toán, xử lý bài toán một cách có
hệ thống.

You might also like