Bài giảng Ngôn ngữ lập trình - Bài 8: Đa hình và hàm ảo - Lê Nguyễn Tuấn Thành
Tóm tắt Bài giảng Ngôn ngữ lập trình - Bài 8: Đa hình và hàm ảo - Lê Nguyễn Tuấn Thành: ... SỐ BÁN HÀNG (2/2)  Chương trình phải:  Tính toán số lượng lớn bán hàng mỗi ngày  Tính toán lượng bán hàng lớn nhất, nhỏ nhất trong ngày  Có thể là lượng bán hàng trung bình trong ngày  Tất cả đều đến từ những hóa đơn riêng lẻ  Nhưng sau này nhiều hàm để tính hóa đơn sẽ được th...t cả các đối tượng của lớp con DiscountSale!  Thay vì phiên bản mặc định được định nghĩa trong lớp cha Sale!  Nhớ lại: lớp Sale được viết trước lớp con DiscountSale  Hàm thành viên savings và toán tử < được biên dịch ngay cả trước khi có ý tưởng về tạo lớp con DiscountSale!  Dis...trước:  Một đối tượng DiscountSale “là” một Sale, nhưng điều ngược lại không đúng 23 TƯƠNG THÍCH KIỂU – VÍ DỤ  class Pet { public: string name; virtual void print() const; }; class Dog : public Pet { public: string breed; virtual void print() const; }; 24 SỬ...
NGÔN NGỮ LẬP TRÌNH 
Bài 8: 
Đa Hình và Hàm Ảo 
Giảng viên: Lê Nguyễn Tuấn Thành 
Email: thanhlnt@tlu.edu.vn 
Bộ Môn Công Nghệ Phần Mềm – Khoa CNTT 
Trường Đại Học Thủy Lợi 
NỘI DUNG 
1. Đa hình (Polymorphism) 
2. Cơ bản về Hàm ảo (Virtual Function) 
 Gắn kết trễ (Late binding) 
 Cài đặt hàm ảo 
 Khi nào sử dụng hàm ảo? 
 Hàm ảo thuần (Pure Virtual Function) và 
Lớp trừu tượng (Abstract Class) 
3. Con trỏ và Hàm ảo 
 Mở rộng tương thích kiểu 
 Ép kiểu lên (Upcasting) 
 Ép kiểu xuống (Downcasting) 
2 
Bài giảng có sử dụng hình vẽ trong cuốn sách “Practical Debugging in C++, 
A. Ford and T. Teorey, Prentice Hall, 2002” 
ĐA HÌNH 
(POLYMORPHISM) 
 Một trong ba trụ cột quan trọng trong OOP 
 Đa hình (Polymorphism) là hiện tượng các đối 
tượng thuộc các lớp khác nhau hiểu cùng một 
thông điệp theo các cách khác nhau 
 Ví dụ: cùng là thông điệp “nhảy”, một con 
kangaroo và một con cóc sẽ nhảy hai kiểu khác 
nhau. 
 Chúng có cùng hành vi “nhảy” nhưng nội dung của 
hành vi này là khác nhau 
3 
CƠ BẢN VỀ HÀM ẢO 
 Hàm ảo 
 Hàm ảo cung cấp khả năng đa hình này 
 Hàm có thể được “sử dụng” trước khi thực sự được định 
nghĩa 
4 
VÍ DỤ VỚI CÁC LỚP MÔ TẢ HÌNH VẼ (1/5) 
HÀM THÀNH VIÊN DRAW() 
 Xây dựng các lớp cho nhiều kiểu hình vẽ khác 
nhau 
 Hình chữ nhật, hình tròn, hình oval  
 Mỗi hình cụ thể là đối tượng của những lớp này 
 Dữ liệu hình chữ nhật: chiều cao, chiều rộng 
 Dữ liệu hình tròn: tâm, bán kính 
 Tất cả các lớp này đều kế thừa từ một lớp cha: 
Figure 
 Các lớp này đều có hàm draw() 
 Mục đích là vẽ hình này trên màn hình 
 Mỗi lớp có cài đặt khác nhau tương ứng với mỗi loại 
hình vẽ 5 
VÍ DỤ VỚI CÁC LỚP MÔ TẢ HÌNH VẼ (2/5) 
SỬ DỤNG HÀM THÀNH VIÊN DRAW() 
 Mỗi lớp con cần định nghĩa hàm draw() riêng 
 Có thể gọi hàm draw() của mỗi lớp, ví dụ: 
 Rectangle r; 
Circle c; 
r.draw(); // Gọi hàm draw của lớp Rectangle 
c.draw(); // Gọi hàm draw của lớp Circle 
 Điều này là bình thường, chưa có gì đặc biệt ở đây! 
6 
VÍ DỤ VỚI CÁC LỚP MÔ TẢ HÌNH VẼ (3/5): 
HÀM THÀNH VIÊN CENTER() 
 Lớp cha Figure bao gồm những hàm có thể áp 
dụng cho “tất cả” hình vẽ 
 Xét hàm center() để di chuyển một hình vẽ từ vị 
trí hiện tại tới vị trí trung tâm màn hình 
 Cách làm: xóa hình vị ở vị trí hiện tại, sau đó vẽ lại tại 
vị trí trung tâm màn hình 
 Hàm Figure::center() sẽ sử dụng (gọi) hàm draw() để 
vẽ lại hình 
 Câu hỏi: 
 Hàm draw() nào sẽ được gọi? 
 Từ lớp nào? 
7 
VÍ DỤ VỚI CÁC LỚP MÔ TẢ HÌNH VẼ (4/5): 
ĐỊNH NGHĨA LỚP HÌNH VẼ MỚI 
 Xét một lớp hình vẽ mới: lớp Triangle kế thừa từ lớp 
Figure 
 Hàm center() của lớp Triangle kế thừa từ lớp cha 
Figure 
 Liệu hàm này có hoạt động được với lớp Triangle? 
 Hàm này sử dụng hàm draw() riêng của lớp Triangle! 
 Nếu hàm này sử dụng hàm Figure::draw() -> không hoạt 
động đúng với lớp Triangle 
 Muốn: kế thừa hàm center() để sử dụng hàm 
Triangle::draw() chứ KHÔNG PHẢI hàm 
Figure::draw() 
 Nhưng lớp Triangle CHƯA ĐƯỢC định nghĩa khi hàm 
Figure::center() định nghĩa! 
 Không biết sự tồn tại lớp Triangle 
8 
VÍ DỤ VỚI CÁC LỚP MÔ TẢ HÌNH VẼ (5/5): 
HÀM ẢO 
 Hàm ảo là câu trả lời cho vấn đề trên 
 Nói với trình biên dịch: 
 Không biết hàm sẽ được cài đặt như thế nào 
 Đợi cho đến khi được sử dụng trong chương trình 
 Sau đó lấy phần cài đặt từ đối tượng cụ thể 
 Được gọi là gắn kết trễ (late binding) hoặc gắn kết 
động (dynamic binding) 
 Những hàm ảo cài đặt cơ chế late binding 
9 
VÍ DỤ DOANH SỐ BÁN HÀNG (1/2) 
 Xây dựng chương trình giúp lưu trữ hồ sơ cho một 
cửa hàng phụ tùng ô tô. 
 Mục đích: lưu trữ doanh số bán hàng 
 Không lường trước hết tất cả loại doanh số bán hàng 
 Đầu tiên chỉ là doanh số bán lẻ thông thường 
 Sau đó: doanh số bán hàng giảm giá, doanh số bán 
hàng qua thư điện tử,  
 Phụ thuộc vào nhiều yếu tố như giá, thuế  
10 
 VÍ DỤ DOANH SỐ BÁN HÀNG (2/2) 
 Chương trình phải: 
 Tính toán số lượng lớn bán hàng mỗi ngày 
 Tính toán lượng bán hàng lớn nhất, nhỏ nhất trong 
ngày 
 Có thể là lượng bán hàng trung bình trong ngày 
 Tất cả đều đến từ những hóa đơn riêng lẻ 
 Nhưng sau này nhiều hàm để tính hóa đơn sẽ được 
thêm vào! 
 Khi những loại doanh số bán hàng khác nhau được 
thêm vào 
 Vì thế hàm để tính toán một hóa đơn sẽ là hàm 
ảo! 11 
ĐỊNH NGHĨA LỚP SALE 
 class Sale 
{ 
public: 
 Sale(); 
 Sale(double thePrice); 
 double getPrice() const; 
 virtual double bill() const; 
 double savings(const Sale& other) const; 
private: 
 double price; 
}; 
12 
HÀM THÀNH VIÊN SAVINGS VÀ 
TOÁN TỬ < 
 double Sale::savings(const Sale& other) const 
{ 
 return (bill() – other.bill()); 
} 
 bool operator < ( const Sale& first, 
 const Sale& second) 
{ 
 return (first.bill() < second.bill()); 
} 
 Lưu ý: CẢ HAI hàm này đều sử dụng hàm bill()! 
13 
LỚP SALE 
 Biểu diễn doanh số bán hàng cho mỗi mục đơn lẻ 
mà không tính tới yếu tố giảm giá hay phí tăng 
thêm 
 Chú ý từ khóa virtual trong khai báo của hàm 
thành viên bill() 
 Tác dụng: sau đó, những lớp kế thừa của lớp Sale có 
thể định nghĩa những phiên bản hàm bill() của riêng 
chúng 
 Những hàm thành viên khác của lớp Sale sẽ sử dụng 
phiên bản hàm bill() dựa trên đối tượng của lớp con! 
 Chúng sẽ không tự động sử dụng phiên bản hàm bill() 
của lớp cha Sale! 
14 
ĐỊNH NGHĨA LỚP CON DISCOUNTSALE 
 class DiscountSale : public Sale 
{ 
public: 
 DiscountSale(); 
 DiscountSale( double thePrice, 
 double the Discount); 
 double getDiscount() const; 
 void setDiscount(double newDiscount); 
 double bill() const; 
private: 
 double discount; 
 }; 
 15 
CÀI ĐẶT HÀM BILL CỦA LỚP CON 
DISCOUNTSALE 
 double DiscountSale::bill() const 
{ 
 double fraction = discount/100; 
 return (1 – fraction)*getPrice(); 
} 
 Từ khóa virtual không xuất hiện trong cài đặt thực tế 
của hàm ảo 
 Tự động là hàm ảo trong lớp con 
 Khai báo (trong giao diện) cũng không yêu cầu phải có từ 
khóa virtual (nhưng thường được sử dụng) 
 Hàm ảo trong lớp cơ sở sẽ tự động là hàm ảo trong lớp 
kế thừa 
 Khai báo lớp con (trong giao diện) 
 Không yêu cầu phải có từ khóa virtual 
 Nhưng có thể viết thêm để dễ đọc, dễ phân biệt 
16 
LỚP CON DISCOUNTSALE 
 Hàm thành viên bill() của lớp DiscountSale được cài 
đặt khác so với hàm này trong lớp cha Sale 
 Riêng biệt cho việc bán hàng giảm giá 
 Hàm thành viên savings và toán tử < 
 Sẽ sử dụng định nghĩa này của hàm bill() cho tất cả các đối 
tượng của lớp con DiscountSale! 
 Thay vì phiên bản mặc định được định nghĩa trong lớp cha 
Sale! 
 Nhớ lại: lớp Sale được viết trước lớp con DiscountSale 
 Hàm thành viên savings và toán tử < được biên dịch ngay 
cả trước khi có ý tưởng về tạo lớp con DiscountSale! 
 DiscountSale d1; 
d1.savings(d2); 
 Lời gọi trong hàm savings này tới hàm bill() sẽ biết sử dụng 
định nghĩa hàm bill() từ lớp DiscountSale! 
17 
THỰC THI HÀM ẢO BẰNG CÁCH NÀO? 
 Để giải thích liên quan đến khái niệm gắn kết trễ 
(late binding) 
 Hàm ảo cài đặt late binding 
 Nói trình biên dịch đợi cho đến khi hàm được sử dụng 
trong chương trình 
 Quyết định phiên bản nào của hàm được sử dụng dựa 
trên đối tượng gọi 
 Một khái niệm rất quan trọng trong OOP 
18 
GHI ĐÈ (OVERRIDING) 
 Định nghĩa hàm ảo thay đổi trong một lớp kế thừa 
 Chúng ta gọi đó là “ghi đè” (overidden) 
 Khác với nạp chồng (overloading) như thế nào ? 
 Tương tự như định nghĩa lại cho các hàm chuẩn 
 Phân biệt: 
 Hàm ảo thay đổi: ghi đè (overidden) 
 Hàm bình thường thay đổi: định nghĩa lại (redefined) 
19 
ĐIỂM YẾU CỦA VIỆC SỬ DỤNG HÀM ẢO 
 Bỏ qua tất cả những lợi ích của hàm ảo như chúng 
ta đã thấy 
 Hàm ảo có một bất lợi lớn: phụ phí (overhead)! 
 Sử dụng nhiều bộ nhớ hơn 
 Gắn kết trễ (late binding) khiến chương trình chạy 
chậm hơn 
 Vì vậy nếu hàm ảo không thật cần thiết thì không 
nên sử dụng 
20 
HÀM ẢO THUẦN 
(PURE VIRTUAL FUNCTIONS) 
 Lớp cơ sở có thể không có định nghĩa có nghĩa cho 
một vài thành viên của nó! 
 Mục đích của nó đơn giản là để cho những lớp khác kế 
thừa 
 Nhớ lại lớp Figure 
 Tất cả các hình vẽ là đối tượng của lớp kế thừa cụ thể. 
Ví dụ: Rectangle, Circle, Triangle,  
 Lớp Figure không có ý niệm về việc bằng cách nào có 
thể vẽ được! 
 Tạo một hàm ảo thuần: 
 virtual void draw() = 0; 
21 
LỚP CƠ SỞ TRỪU TƯỢNG 
(ABSTRACT BASE CLASSES) 
 Các hàm ảo thuần không yêu cầu định nghĩa 
 Bắt buộc các lớp kế thừa phải định nghĩa phiên bản 
hàm riêng của nó 
 Lớp với một hay nhiều hàm ảo thuần gọi là: lớp cơ 
sở trừu tượng 
 Chỉ có thể được sử dụng như lớp cơ sở 
 Không thể tạo đối tượng từ lớp trừu tượng này. Bởi vì 
nó không có định nghĩa hoàn thiện của tất cả các 
thành viên! 
 Nếu lớp thừa kế không định nghĩa tất cả hàm ảo 
thuần => Nó cũng sẽ là một lớp cơ sở trừu tượng 
22 
MỞ RỘNG TƯƠNG THÍCH KIỂU 
(TYPE COMPATIBILITY) 
 Giả sử D là lớp kế thừa từ lớp cơ sở B 
 Đối tượng của lớp D có thể được gán cho đối tượng của 
lớp cơ sở B 
 Nhưng ngược lại thì không thể! 
 Xét ví dụ trước: 
 Một đối tượng DiscountSale “là” một Sale, nhưng điều 
ngược lại không đúng 
23 
TƯƠNG THÍCH KIỂU – VÍ DỤ 
 class Pet 
{ 
public: 
 string name; 
 virtual void print() const; 
}; 
class Dog : public Pet 
{ 
public: 
 string breed; 
 virtual void print() const; 
}; 
24 
SỬ DỤNG HAI LỚP PET VÀ DOG 
 Xét khai báo sau: 
 Dog vdog; 
Pet vpet; 
 Chú ý các biến thành viên name và breed đều 
public! Chỉ nhằm mục đích minh họa 
 Tất cả mọi thứ “là” dog thì đều “là” pet 
 vdog.name = "Tiny"; 
vdog.breed = "Great Dane"; 
vpet = vdog; 
 Có thể gán giá trị về kiểu của lớp cha, nhưng 
không có chiều ngược lại 
 Một pet “không là” một dog 
25 
VẤN ĐỀ MẤT MÁT THÔNG TIN 
(SLICING) 
 Chú ý khi giá trị được gán về vpet, biến thành 
viên breed của nó bị mất đi 
 cout << vpet.breed; // sẽ tạo ra một thông báo lỗi 
 Được gọi là vấn đề mất mát thông tin (slicing) 
 Điều này là hợp lý 
 Khi đối tượng của lớp Dog chuyển thành đối tượng của 
lớp Pet, nó sẽ được đối xử như một Pet 
 Do đó không còn các thuộc tính của một Dog 
 Vấn đề slicing gây phiền toái 
 vpet vẫn là một Greet Dane có tên là Tiny 
 Chúng ta muốn tham chiếu đến biến thành viên breed 
của nó kể cả khi nó được đối xử như một Pet 
 Có thể làm thế với con trỏ trỏ đến những biến động 
26 
GIẢI QUYẾT VẤN ĐỀ SLICING 
 Pet *ppet; 
Dog *pdog; 
pdog = new Dog; 
pdog->name = "Tiny"; 
pdog->breed = "Great Dane"; 
ppet = pdog; 
 Không thể truy cập trường breed của đối tượng 
được trỏ tới bởi pet: 
 cout breed; // Không hợp lệ! 
 Phải sử dụng hàm ảo thành viên: ppet->print(); 
 Gọi hàm thành viên print() trong lớp Dog! 
 Bởi vì nó là hàm ảo 
 C++ sẽ đợi để nhìn đối tượng con trỏ nào mà ppet thực 
sự trỏ tới trước khi lời gọi được gắn kết (binding) 
27 
HÀM HỦY ẢO 
(VIRTUAL DESTRUCTORS) 
 Hàm hủy cần giải phóng động dữ liệu được cấp 
phát 
 Xét ví dụ: 
 Base *pBase = new Derived; 
delete pBase; 
 Sẽ gọi hàm hủy của lớp cơ sở mặc dù pBase đang trỏ 
tới đối tượng của lớp Derived! 
 Xây dựng hàm hủy ảo sẽ giải quyết vấn đề này! 
 Cách tốt là định nghĩa tất cả hàm hủy là hàm ảo 
28 
ÉP KIỂU (CASTING) 
 Xét ví dụ: 
 Pet vpet; 
Dog vdog; 
vdog = static_cast(vpet); // Không hợp lệ! 
 Không thể ép một pet thành một dog, nhưng: 
 vpet = vdog; // Hợp lệ! 
vpet = static_cast(vdog); // Hợp lệ! 
 Ép kiểu lên (upcasting) là hợp lệ 
 Từ kiểu con cháu lên kiểu tổ tiên 
29 
ÉP KIỂU XUỐNG (DOWNCASTING) 
 Ép kiểu xuống rất nguy hiểm! 
 Ép từ kiểu tổ tiên thành kiểu con cháu 
 Giả sử thông tin được thêm vào 
 Có thể được thực hiện với dynamic_cast 
 Pet *ppet; 
ppet = new Dog; 
Dog *pdog = dynamic_cast(ppet); 
 Hợp lệ, nhưng nguy hiểm 
 Ép kiểu xuống hiếm khi dùng do một số nhược 
điểm 
 Phải kiểm tra xem tất cả thông tin có được thêm vào 
hay không 
 Tất cả hàm thành viên phải là hàm ảo 
30 
TÓM TẮT 
 Gắn kết trễ (late binding) trì hoãn quyết định về việc 
hàm thành viên nào được gọi cho đến khi chạy chương 
trình 
 Trong C++, hàm ảo sử dụng cơ chế gắn kết trễ 
 Hàm ảo thuần không có định nghĩa 
 Một lớp với ít nhất một hàm ảo thuần gọi là lớp trừu tượng 
 Không thể tạo đối tượng từ lớp trừu tượng 
 Được sử dụng chặt chẽ như là cơ sở của những lớp kế thừa 
khác 
 Đối tượng của lớp kế thừa có thể được gán cho đối 
tượng của lớp cơ sở 
 Có thể một vài thông tin của lớp kế thừa bị mất => vấn đề 
cắt lát 
 Gán con trỏ và đối tượng động cho phép giải quyết vấn đề 
mất mát thông tin (slicing) 
 Nên định nghĩa tất cả hàm hủy là hàm ảo 
 Đảm bảo bộ nhớ được giải phóng đúng cách 31 
GIÁO TRÌNH THAM KHẢO 
 Giáo trình chính: W. Savitch, Absolute C++, 
Addison Wesley, 2002 
 Tham khảo: 
 A. Ford and T. Teorey, Practical Debugging in C++, 
Prentice Hall, 2002 
 Nguyễn Thanh Thủy, Kĩ thuật lập trình C++, NXB 
Khoa học và Kĩ Thuật, 2006 
32 
File đính kèm:
 bai_giang_ngon_ngu_lap_trinh_bai_8_da_hinh_va_ham_ao_le_nguy.pdf bai_giang_ngon_ngu_lap_trinh_bai_8_da_hinh_va_ham_ao_le_nguy.pdf





