Chương 5
KỸ THUẬT KẾ THỪA
5.1 LỢI ÍCH CỦA VIỆC KẾ THỪA
Sự kế thừa là một phần không thể thiếu của lập trình theo hướng đối tượng Nó là một lợi thế, cho phép ta có thể sử dụng lại mã lệnh Một lớp cơ sở được viết và biên dịch, nó không cần thiết phải biên dịch lại Tuy nhiên, việc sử dụng kế thừa có thể được đưa vào các tình huống khác nhau Sử dụng lại mã lệnh sẽ tiết kiệm thời gian và làm tăng tính tin cậy của chương trình Sự kế thừa cũng có thể giúp ích trong mô phỏng các vấn đề và thiết kế toàn
diện của chương trình
Một kết quả quan trọng của tính sử dụng lại là dé dang phân phối các
thư viện lớp Người lập trình có thể sử dụng một lớp đã được tạo ra bởi một
ai đó hay một công ty mà không cần phải sửa đổi nó
5.2 TÍNH KẾ THỪA
5.2.1 Lớp kế thừa
Một lớp được xây dựng và kế thừa từ một lớp khác gọi là !ớp kế thừa
Trang 2Hơn thế nữa, một lớp có thể là cơ sở cho nhiều lớp kế thừa khác nhau Đến lượt mình, lớp kế thừa lại có thể dùng làm lớp cơ sở để xây dựng các lớp kế thừa
khác; ngoài ra nó có thể kế thừa từ nhiều lớp cơ sở (hình 5.2 và 5.3) Khi xây dựng lớp, người ta thường dùng mũi tên đi lên nhằm chỉ ra lớp ở dưới kế Động vật Động vật có vú Loài bò sát thừa lớp ở trên Ran Than lan Ngua Chó Hình 5.2 Kiến trúc phân tầng kế thừa của một số loài động vật Công dân >> Người quản lý Cán bộ giảng dạy Trưởng bộ môn Hình 5.3 Đa kế thừa
Trang 35.2.2 Cách xây dựng lớp kế thừa Giả sử ta đã định nghĩa lớp A, để xây dựng lớp C là kế thừa từ lớp A ta viết: class C: public A { 1z
5.2.3 Kế thừa private, public và thuộc tính protected
Nếu muốn định nghĩa lớp C kế thừa từ hai lớp A và B đã định nghĩa, ta viết: class C: public A, public B
{ he
Ta thấy rằng, ở khai báo trên, lớp C kế thừa theo kiểu public Nếu
thay public bang private thì nó sẽ kế thừa theo kiểu private
Với khai báo:
class C: public A, B
{
Mi
thì lớp A là lớp cơ sở pub1ic của lớp C, lớp B là lớp cơ sở private của lớp C Ta có các tinh chat sau:
— Nếu kế thừa theo kiểu public thi tất cả thành phần pub1lic của lớp
cơ sở sẽ trở thành thành phần public của lớp kế thừa -
— Nếu kế thừa theo kiểu pzivate thì tất cả thành phần pub1ic của lớp cơ sở sẽ trở thành thành phần private của lớp kế thừa
— Tuy lớp kế thừa kế thừa các thành phần dữ liệu và phương thức của
lớp cơ sở, nhưng ở trong các phương thức của lớp kế thừa không được phép
Trang 4truy cập trực tiếp đến các thành phân dữ liệu private của lớp cơ sở Để khắc phục điều này, ta thay từ khoá private của lớp cơ sở thành từ khoá
protected |
— Lép ké thita kế thừa được các phương thức của lớp cơ SỞ, trừ Các
phương thức sau đây thì không được kế thừa: hàm tạo, hàm huỷ, toán tử gan
5.3 DON KE THUA |
Đơn kế thừa là một lớp kế thừa duy nhất từ một lớp khác (hình 5.1) Trong thế giới thực xung quanh chúng ta có rất nhiều đối tượng thuộc một lớp nào đó là kế thừa từ lớp khác Ví dụ sau xây lớp Congdan, trong lớp này có: họ tên, quê quán, số chứng minh thư và các phương thức thành viên,
tiếp đến ta xây dựng lớp Tai khoan kế thừa lớp Congdan Vi du 5.1 Lớp tài khoản kế thừa lớp công dân #include<iostream.h> #include<string.h> #include<iomanip.h> #include<conio.h> class Congdan { private:
char ht{ 20}, qal 30); //ht:ho tén, qq: quê quán
Trang 5/ stk: s6 tài khoản, sđ: số dư tài khoản, pin: mã pin long int stk,., sd; long int pin; public: Taikhoan() { stk= 0; sd= 0; pin= 0; } void rhap ()-; void hienthi(); Jie 150 © void Congdan: :nhap () {
cout<<"\nHo va ten: "; cin.getline(ht, 20);
cout<<"Que quan: "; cin.getline (qq, 30); cout<<"So CMT: "; cin>>cmt; } void Congdan: :hienthi () { Cout<<ht<<setw (10) <<qq<<setw (10)<<emt¿ } void Taikhoan: :nhap() { " Congdan: :nhap();
cout<<"So tai khoan: "; cin>>stk; cout<<"Ma pin: "; cin>>pin;
Trang 6Taikhoan a,b; cout<<"\Nnhap TAI KHOAN A\n"; a.nhap(); cín.ignore (1); cout<<"\Nnhap TẠI KHOAN B\n"; b.nhap();
cout<<"\nTai khoan a\n"; a.hienthi();
¡ cout<<"\nTai khoan b\n”; b.hienthi();
getch();
}
Kế quả:
NHAP TAI KROAN A
Ho va ten: Nguyen Tuan Anh
Que quan: Bac Ninh
So CMT: 1250
So tai khoan: 1234
Ma pnhap: 234569
So du tai khoan: 1000000
NHAP TAI KHOAN B
Ho va ten: Nguyen Quang Khanh Que quan: Thai Binh So CMT: 13456 So tai khoan: 12345 Ma pnhap: 123456 So du tai khoan: 2000000 Tai khoan À: Nguyen Tuan Anh Bac Ninh 1250 1234 234569 -1000000 Tai khoan B Nguyen Quang Khanh Thai Binh 13456 12345 123456 2000000
5.4 HẦM TẠO LỚP KẾ THỪA VÀ HÀM TẠO LỚP CƠ SỞ
Trong phần này sử dụng hai lớp là Congdan và Tai khoan, ta kiểm tra
sự hoạt động của hàm tạo lớp cơ sở và hàm tạo lớp kế thừa Chương trình dưới đây khai báo một đối tượng thuộc lớp kế thừa rồi nhập và hiển thị dữ
liệu của nó lên màn hình
Trang 7Ví dụ 5.2 Hàm tạo lớp kế thừa và hàm tạo lớp cở sở #include <iostream.h> #include <string.h> #include <iomanip.h> #include <conio.h> class Congdan ( private: char ht[ 20], gợi 30] ; long int cmt; public: Congdan () { cout<<"\nHam tao lop co so"; strepy(ht," "); strcpy(qq," "); cmt= 0; } void nhap(); void hienthi(); he void Congdan: :nhap () {,
cout<<"\nNhap cong dan\n";
cout<<"\nHo va ten: "; cin.getline(ht,20);
Trang 8cout<<"\nHam tao lop co ke thua”; stk= 0; sd= 0; pnhap= 0; } ‘void nhap(); void hienthi(); LG ‘void Taikhoan: :nhap () { Congdan: :nhap ();
' eout<<"So tai khoan: "; cỉn>>stk; cout<<"Ma PIN: "; cin>>pnhap;
cout<<"So du tai khoan: "; cin>>sd; } void Taikhoan: :hienthi () { Congdan::hienthi (); cout<<setw (10)<<stk<<setw (10)<<pnhap<<setw (10)<<sd; } void main() { clirscr(); Taikhoan a; a.nhap()z + a,hienthi(); getch(); } Két qua:
Ham tao lop co so Ham tao lop co ke thua
Nhap cong dan
Trang 9Từ kết quả nhận được ta thấy rằng, khi đối tượng của lớp Taikhoan
được tạo thì đầu tiên hàm tạo lớp cơ sở Congdan được gọi trước, sau đó hàm
tạo lớp Tai khoan mới được gọi
Trong nhiều tình huống muốn nạp chồng hàm tạo lớp kế thừa để khai báo và khởi tạo đối tượng với các giá trị ban đầu, chẳng hạn:
Tai khoan (char ht[], char qdg[],long int.scmt, long _ dint sotk, long int mapin,long int sodu): Congdan (ht, qq, scmt) cout<<"\nHam tao lop ke thua duoc nap chong\n"; stk= sotk; ` sd= sodu; pin= mapin; }
Do lớp Taikhoan ké thita lé6p Congdan, do đó phải truyền giá trị họ tên, quê quán và số chứng minh thư cho lớp cơ sở, nên phải nạp chồng hàm tạo lớp cơ sở Congdan ‘
Trang 101 1 Congdan(char hoten{], char quequan[], long int scmt) { cout<<"\nHam tao lop co so duoc, nap chong\n"; strepy (ht, hoten); strepy (qq, quequan) ; cmt=scmt; } void nhap(); :void hienthi(); }; void Congdan: :nhap() {
cout<<"\nNHAP CONG DAN\n";
cout<<"\nHo va ten: "; cin.getline(ht,20); cout<<"Que quan: "; cin.getline (qq, 30); cout<<"So CMT: "; cin>>¢mt; } void Congdan: :hienthi () { cout<<ht<<setw (10) <<qq<<setw(10)<<emt; do : class Taikhoan: public Congdan { private: long int stk, sd; long int pin; public: Tai khoan () { ' cout<<"\nHam tao lop co ke thua\n"; stk= 0; sd= 0 pin= 0; ` }
Taikhoan(char ht{], char qq[},long int scmt, long int sotk, long int mapin,long int sodu): Congdan (ht, qq, scmt)
Trang 11{ cout<<"\nHam tao lop ke thua duoc nap chong\n"; stk= sotk; sd= sodu; pin= mapin; void nhap(); void hienthi(); Mi void Taikhoan: :nhap() { Congdan: :nhap (};
cout<<" So tai khoan: "; cin>>stk; Cout<<"Ma PIN: "; cin>>pin;
Trang 12Kết quả:
| TAI KHOAN a
Ham tao lop co so Ham tao lop co ke thua Nhap cong dan
Ho va ten: Nguyen Tuan Anh Que quan: Bac Ninh
So CMT: 12345 , So tai khoan: 884192 Ma PIN: 68841
So.du tai khoan: 100000
Nguyen Tuan Anh Bac Ninh 12345 884192 68841 100000
TAI KHOAN b
Ham tao lop co so duoc nap chong Ham tao lop ke thua duoc nap chong
Nguyen Gia Bao Ha noi 123456 654355356 6884192 20000000
5.5 THỨ TỰ GỌI HẦM TẠO CỦA LỚP CƠ SỞ VÀ LỚP KẾ THỪA
Trang 13char ht[ 20], qaf 30]; long int emt; public: Congdan (} { strcpv(ht," "); strcpy(qq," "); cmt= 0; } Congdan (char htị 20], char ga 30), long int cmt) ' - : - : cout<<"\nTao doi tuong cong dan\n"; strcpy (this->ht,ht); Strcby (this->qq, qq) ; this->cmt=cmt; } void nhap(); void hienthi(); Me class Taikhoan: public Congdan { ˆ ` Ppriväte: long int stk, sd; long int pin; public: » Taikhoan() fo, stk= 0; sd= 0; pin= 0; } ,
Trang 14this->pin=pin;
}
void nhap(); void hienthi();
int operator >(Taikhoan b); int operator <(Taikhoan b);
biG
void Congdan::nhap()
{
cin.ignore (1);
cout<<"\nHo va ten: ";cin.getline (ht,20); cout<<"Que quan: "; cin.getline (qq, 30); cout<<"So CMT: "; cin>>cmt; } void Congdan::hienthi () { cout<<ht<<setw (10) <<qq<<setw(10)<<cmt; } void Taikhoan: :nhap() { Congdan: :nhap();
cout<<"So tai khoan: "7 cin>>stk; cout<<"Ma pin: "; cin>>pin;
Trang 15int kq= 1; if (this->sd >= b.sd) kq= 0; else kq= l1; return kq; } void main () { clrscr(); Taikhoan a ("Nguyen Tuan Anh" ,"Thuan Thanh, Bac Ninh",125066999,12345,2000000,123456); cCout<<"\nTai khoan a\n"; a.hienthi(); getch(); } Két qua:
Tao doi tuong cong dan
Tao doi tuong tai khoan Tai khoan a Nguyen Tuan Anh Thuan Thanh, Bac Ninh 125066999 12345 123456 2000000
5.6 HAM TAO SAO CHEP CUA LOP CO SO
Trong chương 3 ta đã biết lý đo tại sao phải xây dựng hàm tạo sao chép, chính vì sự có mặt của các thành phần đữ liệu là con trỏ cho nên nó đã làm thay đổi dữ Hiệu của biến đối tượng (xe1) khi khai báo và khởi tạo giá trị ban _đầu cho một đối tượng khác (xe2) bởi giá trị của đối tượng này
Như vậy, cần xây dựng hàm tạo sao chép, bởi vì trong lớp cơ sở cũng có thể có thành phần dữ liệu là con trỏ Cũng có khi ta khai báo và khởi tạo giá trị cho một đối tượng của lớp cơ sở bởi giá trị của đối tượng cùng lớp Lập luận tương tự đối với lớp kế thừa nếu có thành phần dữ liệu là con trỏ thì phải xây dựng hàm tạo sao chép cho lớp
Ví dụ dưới đây khai báo thành phần số chứng minh thư là con trỏ trỏ
Trang 17162 public: Tai khoan () { stk= 0; sd= 0; pin= 0; }
Taikhoan(char ht{ 20], char qq 30], long int cmt, long int stk, long int sd, long int pin): Congdan (ht, qq, cmt) , cout<<"\nTao doi tuong tai khoan\n"; this->stk=stk; this->sd =sd; this->pin=pin; } void nhap(); void hienthi(); MG void Congdan: :nhap () { long int x; 'eout<<"\nHo va ten: "; cin.getline(ht,20); cin.ignore (1);
cout<<"Que quan: "; cin.getline (qq, 30);
cout<<"So CMT: "; cin>>x; * (this->cmt)=x;; } void Congdan: :hienthi () { cout<<ht<<setw(10)<<" "<<qq<<setw(10)<<* cmt; } void Taikhoan: :nhap () { Congdan: :nhap ();
cout<<"So tai khoan: "; cin>>stk;
‘cout<<"Ma pin: "; cin>>pin;
cout<<"So du tai khoan: "; cin>>sd;
}
Trang 18Congdan: :hienthi(); cout<<setw (10)<<stk<<setw (10) <<pin<<setw (10)<<sd; } void main() { clrscr();long int cmt; Taikhoan a("Nguyen Tuan Anh","Thuan Thanh, Bac Ninh", 125066999,12345, 2000000, 123456); cout<<"\nTai khoan a\n"; a.hienthi(); Taikhoan b(a);
cout<<"\nTai khoan b\n"; b.hienthi();
cout<<"\nThay doi so chung minh thu cua b la:";
cin>>cmt; '
b.thaydoicmt (cmt);
cout<<"\n Sau khi thay doi so chung minh thu cua b\n"; ,cout<<"\nTai khoan a\n"; a.hienthi();
cout<<"\nTai khoan b\n"; b.hienthi(); getch(); } Kết quả:
‘Tao doi tuong cong dan Tao doi tuong tai khoan Tai khoan a Nguyen Tuan Anh Thuan Thanh, Bac Ninh 125066999 12345 123456 2000000 Tai khoan b : ‘Nguyen Tuan Anh Thuan Thanh, Bac Ninh 125066999 12345 123456 2000000
Trang 19Qua kết quả trên ta thấy rằng, khi b thay đổi số chứng minh thư thì số
chứng minh thư của a cũng thay đổi theo và chương trình còn thông báo một cảnh báo là: "NULL pointer assignment", nó cho biết sử dụng thành
phần con trỏ là rỗng, nguyên nhân của sự thay đổi số chứng minh thư của
đối tượng a là do biến cmt của lớp Congdan là con trỏ Bpi vì, khi khai báo
đối tượng b và khởi tạo bởi dữ liệu của đối tượng a như câu lệnh:
Taikhoan b(a);
thì hàm tạo sao chép mặc định sẽ chép nội dung các thành phần của đối tượng a cho đối tượng b tương ứng, con trỏ cmt của đối tượng a và b cùng trỏ
đến một vùng nhớ Do đó, khi b thay đổi số chứng minh thư bằng câu lệnh: b.thaydoicmt (cmt) ;
thì nội dung ô nhớ và cả hai con trd emt cla a và b đều trô đến bị thay đối, điêu này dẫn đến b thay đổi số chứng minh thư thì số chứng minh thư của a cũng thay đổi theo
Để giải quyết được vấn để này, ta xây dựng hàm tạo sao chép cho lớp cơ sở như dưới đây
Trang 20Congdan(char ht[ 20], char qq 30], long int cm) { strcpy (this->ht,ht); strcpy (this->qg; qQ) / this->cmt=new long int; * (this->cmt) =cm; } Congdan(Congdan &p) { ’ strepy(this->ht,p-ht); strepy (this->qq,p.qq)i emt=new long int; * (this->emt)=* (p.cmt); } void nhap(); void hienthi(); void thaydoicmt (long int x) { * (this) ->cmt=x; } he class Taikhoan: ‘public Congdan private: long int stk, sd; long int pin; public: Tai khoan () { stk= 0; sd= 0; pin= 0; }
Taikhoan(char htị 20], char qaf 30}, long int cmt, long int stk, long int sd, long int pin): Congdan (ht, qq, cmt)
this->stk=stk;
Trang 21this->sd =sd; this->pin=pin; } void nhap(); void hienthi(); ad void Congdan: :nhap () { long int x; cin.ignore(1);
cout<<"\nHo va ten: "; cin.getline (ht, 20);
cout<<"Que quan: "; cin.getline (qq, 30); cout<<"So CMT: "; cin>>x; * (this->cmt)=x;; } void Congdan: :hienthi() { cout<<ht<<setw (10) <<qq<<setw(10)<<* emt; } void Taikhoan: :nhap() { Congdan: : nhap () ¿
€out<<"§o tai khoan: ";¿ cin>>stk; cout<<"Ma pin: "; cin>>pin;
Trang 22cout<<"\nThay doi so chung minh thu cua b la:"; cin>>cmt;
b.thaydoicmt (cmt);
' cout<<"\n Sau khi thay doi so chung minh thu cua b\n”; cout<<"\nTai khoan a\n"; a:hienthi();
cout<<"\nTai khoan b\n"; b,hienthi(); _ getch(); } Kết quả: Tai khoan a Nguyen Tuan Anh Thuan Thanh, Bac Ninh 125066999 12345 123456 2000000 Tai khoan Ð / Nguyen Tuan Anh -Thuan Thanh, Bac Ninh 125066999 12345 123456 2000000
Thay doi so chung minh thu cua b 1a:125088888 Sau khi thay doi so chung minh thu cua b Tai khoan a Nguyen Tuan Anh Thuan Thanh, Bac Ninh 125066999 12345 123456 2000000 Tai khoan b Nguyen Tuan Anh Thuan Thanh, Bac Ninh 125088888 12345 123456 2000000 Với kết quả này ta đã giải quyết được điều gặp phải ở trên 5.7 DA KE THUA :
_ Trong thế giới thực có rất nhiều đối tượng có cùng thuộc tính chung
nhất định, những đối tượng này được gom lại thành một lớp, trong mỗi đối
tượng thuộc lớp này có khi lại được cấu tạo bởi một số thành phân mà những thành phần này lại là các đối tượng thuộc một lớp khác, lớp được cấu tạo như
vậy là lớp được kế thừa từ các lớp khác và gọi là lớp đa kế thừa
Trang 23Bộ nhớ trong Bộ nhớ ngồi ¬ Bảng mạch chủ CPU , Bộ nhớ Màn hinh Hộp máy Ban phim Chuột May vi tinh Ð Hình 5.5 Tỉnh đa kế thừa
Hình 5.5 cho thấy lớp Máy vi tính được kế từ các lớp Màn hình, Hộp máy, Bàn phím và Chuột; trong đó lớp Hộp máy lại kế thừa các lớp như là
Bảng mạch chủ, CPU và Bộ nhớ, tron¿ đó lớp Bộ nhớ lại được kế thừa từ lớp
Bộ nhớ trong và Bộ nhớ ngoài
Tính kế thừa rất đa dạng như hình 5.5 làm cho chúng ta khó kiểm soát và xử lý những tình huống xảy ra trong chương trình, vì thế cần giải quyết một số vấn đề sau:
—~ Các hàm tạo và hàm huỷ được gọi như thế nào?
— Sự đa kế thừa cho phép một lớp có thể là lớp kế thừa từ nhiều lớp cơ sở
Trang 24Khai báo lớp C như sau:
class C: public A, public B
{
de
Bên trong lớp C có thể khai báo và định nghĩa các thành phần dữ liệu và các phương thức của lớp C Ta thực hiện lập hàm tạo cho lớp C, để thực hiện
vừa khai báo, vừa khởi tạo giá trị cho đối tượng của lớp C thì phải truyền khai báo dữ liệu cho các thành phần của lớp A, B và C Do đó, phải thực hiện truyền giá trị cho thành phần a, b của từng lớp kế thừa cơ sở A, B và c cho
lớp c Như vậy, phải nạp chồng hàm tạo lớp C như sau:
AB (int a0, int b0, int c0): A (a0), B(b0) {
| this->c = cŨ; có }
Trong hàm tạo lớp C này đã thực hiện truyền giá trị vào cho lớp A và lớp
bB là a0 và b0 tương ứng Như vậy, hàm tạo một đối số của lớp A và của cả
lớp B cũng được triệu gỌI ,
© Thứ tự gọi các hàm tạo: Các hàm tạo của các lớp cơ sở theo thứ tự
khai báo của các lớp cơ sở trong lớp kế thừa lần lượt được gọi, sau cùng là
hàm tạo của lớp kế thừa mới được gọi Trong khai báo và định nghĩa ba lớp A, B và C ở trên, hàm tạo của lớp A được gọi trước, sau đó là hàm tạo của
lớp B, cuối cùng là hàm tạo của lớpC được gọi
e Thứ tự gọi các hàm huỷ: Các hàm huỷ được gọi theo thứ tự ngược lại
với cách gọi các hàm tạo Trong khai báo ba lớp A, B và C ở trên, hàm huỷ của lớp C sẽ được gọi đầu tiên, rồi đến hàm huỷ của lớp B được gọi, cuối cùng là hàm huỷ của lớp A được gọi
ø Các thuộc tỉnh kế thừa: Đa kế thừa cũng sử dụng các thuộc tính kế thừa như kiểu kế thừa đơn
s Cách gọi các hàm thành phân của các lớp cơ sở: Nếu lớp A có hàm hienthi (), lớp B có hàm hienthi(), thì ở lớp C ta sử dụng bàm hienthi () của lớp nào thì phải chỉ rõ phạm vi hàm đó, ví.dụ:
Trang 25void C:: hienthi() {
A::hienthi(); /* sử dụng hàm thành phần của lớp cơ sở A*/
}
Vi du sau minh hoa sự đa kế thừa, ta xây dựng lớp Điểm ảnh — Diemanh là các điểm ảnh tại toạ độ hai chiều (x, y) với độ xám (màu sắc)
hoặc màu nhất định của ảnh số Như vậy, lớp Diemanh sẽ kế thừa hai lớp
Toado2D và lớp Doxam theo kiểu pub1ic (hình 5.7) Toado2D Doxam double x, y int mau; Diemanh
Hình 5.7 Lớp Diemanh kế thừa từ hai lớp
Trang 27172 } Diemanh () { cout<<"\nHam tao mac đỉnh lop Diemanh\n"; }
Diemanh (long int x0, long int y0, unsigned int c): Toado2D (x0, yO) , Doxam(c)
cout<<"\nHam tao mac dinh lop Diemanh \n"; } ~ Diemanh () { cout<<"\nHam huy lop Diemanh\n"; } void hienthi () { Toade2D :: hienthi(); Doxam :: hienthi(); } void main() { clrscr(); DiemanhA (3,4,5); Cout<<" ===================\ nh A.hienthi (); Cout<<" ===================\ nh ¿
cout<<"\n= goi phuong thuc hienthi (})
Trang 28Kết quả:
Ham tao duoc nap chong lop Toado2D
Ham tao nap chong lop Doxam Ham tao mac dinh lop Diemanh Toa do: 3, 4 Mau: 5 ==Ỷ==rc===z========== = goi ham hienthi cua Toado2D Toa do:3, 4 = goi ham hienthi cua Diemanh Mau :5
Ham huy lop Diemanh Ham huy lop Doxam Ham huy lop Toado2D
CÂU HỎI VÀ BÀI TẬP
1 Bạn hãy hình dung là mình sẽ xuất bản một ấn phẩm bao gồm cả sách
và đĩa CD Hãy tạo một lớp Ấn phẩm bao gồm các thành phần là Tên và
Giá của một tài liệu xuất bản Từ đó xây dựng hai lớp trên là Sách và
Đĩa CD mà cả hai đều kế thừa lớp Ấn phẩm, trong đó lớp Sách có thêm các thành phần là Số trang và lớp Đĩa CD có thêm Thời gian chạy tối đa Tất cả ba lớp trên đều có các phương thức như là nhập đữ liệu từ bàn
phím — nhap() và hiển thị dữ liệu lên màn hình - hienthi () Hãy viết chương trình hàm main () để tạo ra đối tượng Sách và Đĩa CD rồi
yêu cầu người dùng nhập và hiển thị đữ liệu của các đối tượng đó lên
màn hình
2 Xây dựng lớp Hệ số của phương trình bậc nhất gồm có hai thành phần là
a và b, rồi viết các phương thức nhập hiển thị hệ số Hãy xây dựng lớp
Phương trình bậc nhất kế thừa lớp Hệ số trên, trong đó có viết phương thức giải phương trình cho lớp này Viết chương trình minh hoạ giải một phương trình bậc nhất với dữ liệu được nhập vào từ bàn phím
Trang 29174
Xây dựng lớp Người gồm có các thành phần Họ tên và Năm sinh Viết
các phương thức của lớp như nhập, hiển thị dữ liệu Xây dựng lớp
Thí sinh kế thừa lớp này, trong lớp Thí sinh có Số báo danh và Điểm các
mơn tốn, lý, hoá Hãy xây dựng thêm các phương thức nhập, xuất, và
khai báo mảng có kiểu là Thisinh để quản lý đanh sách các thi sinh
Viết chương trình với các công việc sau: — Nhập vào một đanh sách có n thí sinh
—_ Hiển thị danh sách thí sinh với tổng điểm ba môn tăng dần;
— Tìm một thí sinh khi biết Số báo danh
- Thống kê xem có bao nhiêu phần trăm thí sinh đạt yêu cầu (cả ba
môn có điểm lớn hơn hoặc bằng 3)
Xây dựng lớp Sản phẩm bao gồm: Mã sản phẩm, Tên sản phẩm, Năm sản xuất và các phương thức nhập, xuất Từ đó xây đựng tiếp lớp Tivi có
thêm các thuộc tính Chiều dài, Chiều rộng của màn hình Hãy xây dựng phương thức tính diện tích của màn hình của lớp Tivi Viết chương trình
nhập các vào đối tượng của lớp Tivi, sau đó sắp xếp tăng dần theo diện
Trang 30Chương 6
TÍNH ĐA HỈNH `
— — ——————DDDDD xxšĩằăưỤN
Ở trên chúng ta đã tìm hiểu một số điều về con trỏ và những cải tiến của C++ như: hàm bạn, lớp bạn, phương thức tĩnh, nạp chồng toán tử, nạp chồng hàm tạo và tạo hàm tạo sao chép Đây không những chỉ là các đặc tính cần
thiết cho lập trình C++ mà nó còn được sử dụng rộng rãi Trong chương này
chúng ta xét một đặc tính hữu ích nữa là phương thức ảo
6.1 PHƯƠNG THỨC ẢO VÀ TÍNH ĐA HỈNH
Ảo có nghĩa là tổn tại nhưng không có thật Khi các phương thức ảo được sử dụng trong một chương trình, nơi xuất hiện lời gợi một phương thức của một lớp có thể là phương thức có thực từ lời gọi trong một lớp khác
Tại sao chúng ta cần phương thức ảo? Bởi vì trong chương trình có thể có những đối tượng khác nhau của các 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 số phép toán và hàm chung cho
các đối tượng này Ví dụ, có một đối tượng là một tam giác, một hình tròn và một hình vuông; trong chương trình ta khai báo các đối tượng hình khác
nhau đó và muốn hiển thị các đối tượng lên màn hình theo cách hiển thị
riêng của từng đối tượng Một cách tiếp cận là tạo ra một mảng mà lưu trữ
được tất cả các đối tượng con trỏ trong cùng một mảng Mang này có thể:
khai báo như sau:
Hinh *ptr[ 100] ;
Biến ptr là một mảng các con trỏ, các con trỏ này thuộc kiểu Hinh
(hay là đối tượng của lớp Hình — ninh), ta sẽ lưu các đối tượng của các lớp
khác nhau vào trong mảng ptz Vậy, làm sao để thực hiện hiển thị được các đối tượng một cách chính xác? Câu hỏi sẽ được trả lời sau khi tìm hiểu và
xem xét ví dụ dưới đây
Trang 31Giả sử ta có ba lớp là Tam giác, Hình tròn và Hình vuông; ba lớp này đều kế thừa lớp Hinh Ta khai báo ba đối tượng abc, c, hv lần lượt là các đối tượng của các lớp Tam giác, Hình tròn và Hình vuông; các đối tượng này được quản lý bởi mảng ptr bằng các câu lệnh:
ptr[ 0] =&abc; //con trỏ ptr[ 0] trỏ đến đối tượng của lớp Tamgiac
ptr[ 1] =&c; //con trỏ ptr[ 1] trỏ đến đối tượng của lớp Hỉnhtron ptr[ 2] =hv; // con trỏ ptr[ 2] trỏ đến đối tượng của lớp Hỉnhvuong
Như vậy, để hiển thị được ba đối tượng của mảng ptr ta thực hiện câu lệnh sau: for(int i = 0; i < 3; itt) ptr[ i} ->hienthi(); Đây là một điều hết sức ngạc nhiên, bởi vì câu lệnh: ptr[ i] ->hienthi();
sẽ thực hiện các phương thức hienthi () khác nhau mà cùng một lời gọi
Nếu con trỏ trong mảng ptr{ i] trỏ đến đối tượng hình tròn thì hàm hienthi () thuộc lớp Hình tròn sẽ được gọi; nếu con trỏ trỏ đến một tam giác thì hàm hienthi () thuộc lớp Tam giác được gọi, và nếu con trổ trỏ
Trang 32Tính đa hình là khả năng thiết kế và cài đặt các hệ thống có thể mở rộng
đễ dàng hơn (hình 6.1) Các chương trình có thể được viết để xử lý tổng
quát, như các đối tượng lớp cơ sở, các đối tượng của tất cả các lớp tồn tại
trong một phân cấp Khả năng cho phép một chương trình sau khi đã biên
dịch có thể có nhiều diễn biến xảy ra là một trong những thể hiện của tính đa hình, tính muôn màu, muôn vẻ của chương trình hướng đối tượng
Một thông điệp được gửi đi (gửi đến đối tượng) mà không cần biết đối tượng nhận thuộc lớp nào Để thực hiện được tính đa hình, các nhà thiết kế C++ cho chúng ta dùng cơ chế kết nối động (dynamic binding) thay cho cơ chế kết nối tĩnh (static binding) ngay khi chương trình biên dịch được dùng trong các ngôn ngữ cổ điển như C, Pascal
Cơ chế kết nối tĩnh là cách mà trình biên dịch sẽ biên dịch tất cả các câu lệnh trong hàm main () cũng như tất cả các hàm tự do và phương thức của
các lớp sang dạng mã máy trước khi chương trình được thực hiện
Cơ chế kết nối động là cách mà chương trình dịch không biên dịch các phương thức ảo khi biên dịch chương trình sang dạng mã máy, mà nó sẽ biên dịch phương thức ảo của từng lớp kế thừa mỗi khi đối tượng của lớp đó gọi phương thức ảo của mình trong khi chương trình đang thực hiện
Tính đa hình là một trong những đặc tính của lập trình hướng đối tượng sau các lớp và tính kế thừa để có thể tiếp cận và làm việc với tính đa hình mà
chúng ta thường xuyên gặp phải Ví dụ 6.1 minh hoạ cho tính đa hình Trước
_tiên ta tạo ra các lớp hình khác nhau như Tamgiac, Hinhtron va Hinhvuong
mà chúng được kế thừa từ lớp cơ sở Hinh Tiếp theo xây dựng các phương thức hienthi () chỉ dùng thực hiện hiển thị lên màn hình thông báo là các xâu ký tự cho biết đối tượng gọi phương thức này là đối tượng thuộc lớp nào, chẳng hạn như: "Đay la Hinh", "Day la Hinh tron”
Trang 34Kết quả: Cac hinh Day la Hinh Day la Hinh Day la Hinh
Các lớp Tamgiac, Hinhtron và Hinhvuong đều kế thừa lớp Hinh,
trong mỗi lớp đều có phương thức hienthi () Trong hàm main () đã tạo ra
ba đối tượng thuộc ba lớp này và một mảng con trỏ thuộc lớp cơ sở, sau đó
cho từng con trỏ của mảng các con trỏ ptr trỏ đến từng đối tượng như câu
lệnh đã nói ở trên
Chúng ta mong đợi là chương trình chạy và hiển thị lên màn hình kết
quả mong muốn là các đối tượng hình được thông báo đúng Chương trình
không những không làm cho chương trình dịch báo lỗi về kiểu dữ liệu địa
chỉ của các đối tượng kế thừa khi cho con-trỏ ptr của lớp kế thừa trỏ đến, mà còn được dịch và thực hiện rất trôi chảy; bởi vì kiểu đữ liệu mà nó kiểm tra có liên quan đến địa chỉ Theo nguyên tắc thì con tré ptr{ i} tro đến các đối tượng của lớp kế thừa là các kiểu dữ liệu tương thích với các con trỏ tới các đối tượng của lớp cơ sở
Bây giờ ta xét đến khi nào thì câu lệnh sau được thực hiện đúng? ptr{ i] ->hienthi();
Phương thức nào được gọi? nó chính là phương thức hienthi () của
lớp cơ sở Với các câu lệnh gán địa chỉ của các biến thuộc lớp kế thừa cho
con trỏ ptr thì các câu lệnh:
ptrfril -> hienthi();
cũng sẽ triệu gọi phương thức hienthi () của lớp cơ sở, kết quả ở ví dụ 6.1 trên cho thấy điều đó
Trên hình 6.2, phương thức hienthi () của lớp cơ sở luôn luôn được
gọi Trình biên dịch đã từ chối nội dung của con trỏ ptr{ i) (dia chi của biến mà nó trỏ đến) và chọn phương thức thành phần phù hợp với kiểu của con trỏ Hình 6.2 cho thấy điều gì đã xây ra khi lớp cơ sở được kế thừa bởi
nhiều lớp với các phương thức cùng tên, và việc triệu gọi cấc phương thức
bằng cách sử dụng các con trỏ nhưng không sử dụng phương thức ảo
Trang 35pH] Lớp Hinh &abc ptr[0]->hienthi() Ì—ˆ tien hienthi(t ) pt[1] &c SN SG ptr[1]}->hienthi() Hinh tam giác Hinh tròn Hinh vuông , ptr{2} hienthi(){ } hienthi({ } hienthi(){ } &hv ptr[2]->hienthi()
Hình 6.2 Sử dụng con trỏ khi phương thức Hienthi() chưa là phương thức ảo
Để phương thức trở thành phương thức ảo, ta đặt từ khoá virtua1 trước kiểu trả lại của phương thức thì phương thức đó sẽ trở thành phương thức ảo
Chẳng hạn, phương thức ảo hienthi () của lớp cơ sở Hinh là:
virtual void hienthi ()
{
cout<<"\n Day la Hinh";
Ta xét xem vấn để là tại sao khi thêm từ khoá virtua1 vào phương
Trang 36Class Tamgiac: public Hinh { public: void hienthi () { cout << "\n Day la Tam giac"; } de class Hinhtron: public Hinh { public: i void hienthi () : {_ cout <<"\nHinh tron"; } ` }z class Hinhvuong: public Hinh ( : public: void hienthi() { cout <<"\n Day la Hinh vuong" ; } }z void main() {
Tamgiac abc; Hinhtron c; Hinhvuong hv; Hinh *pf{ 3] ; //mảng con trổ của lớp cơ sở pl 0] = &abc; BÍ l]= &c; Đ[ 2] = &hv; cout<<"\nCac hinh\n"; for(int i=0; i< 3; i++) pl i] ->hienthi(); } Kết quả: Cac hinh
Day la Tam giac Day la Hinh tron Day la Hinh vuong
Trang 37
Các câu lệnh hàm main () ở ví dụ trên thực hiện như hình 6.3, kết quả
hiển thị như mong muốn ptr[0] Lớp Hinh &abc ptr[0}>hienthiQ hienthi0( } ptf1] : &c eee ptr[1]->hienthi() : Hinh tam giác Hinh tròn Hinh vuông ptr[2] TỐ a |„ hienthi( } hienthi(){ } hienthi(){ } &hv / i ptr[2}->hienthi() a Hình 6.3 Sử dụng phương thức ảo
Chúng ta sẽ để cập đến việc chọn lựa phương thức hiển thị của từng đối tượng cụ thể theo hình mũi tên tương ứng trong hình 6.3
Trong hình 6.3 ta thấy, các phương thức thành phần của các lớp kế thừa được gọi chứ không phải phương thức của cơ sở được gọi Ở đây thay đổi nội dung của con trỏ ptr từ các địa chỉ của các đối tượng abc, c và hv Cac thể hiện của phương thức hienthi () được thực hiện bằng câu lệnh:
ptr[ ¡] ->hienthi ();
sẽ thực hiện các phương thức thành phần dựa vào nội dung của con trổ
ptrf i] theo nguyên tắc là trình biên dịch sẽ lựa chọn phương thức theo nội dung của con trỏ (là địa chỉ của đối tượng mà ptr trỏ đến) chứ không phải theo kiểu của con trỏ nữa Hình 6.3 cho biết các lời gọi lệnh hienthi () là
đúng như mong muốn ,
Trang 386.2 LIEN KET MUON (Late binding) HAY LIEN KET DONG
(Dynamic binding)
Đầu đọc thông mỉnh có thể làm ta ngạc nhiên khi muốn biết làm thế nào
để trình biên dịch biết được phương thức nào được biên dịch khi chạy chương trình
Khi gặp câu lệnh:
ptrí i] ->hienthi();
trình biên dịch luôn luôn biên dịch một lời gọi đến phương thức hienthi ()
trong lớp cơ sở Nhưng khi phương thức hienthi () của lớp cơ sở được gấn đặc tính là phương thức ảo thì trình biên dịch sẽ không biết lớp nào chứa phương thức này; nó có thể là địa chỉ của một đối tượng của các lớp kế thừa, phiền bản (thể hiện) của phương thức hienthi () Trong thực tế, trình biên dịch không biết làm gì, vì thế nó sắp xếp để quyết định trì hoãn lại cho đến
khi chương trình thực hiện mới xử lý Trong thời gian chương trình đang thực hiện, khi được biết một đối tượng nào đó được trỗ đến bởi con trỏ ptrí i],, thì nó sẽ tiếp cận đến phương thức hienthi () của lớp được gọi
Người ta gọi cơ chế này là sự liên kết muộn hay là liên kết động (Việc lựa
chọn các phương thức theo cách thông thường trong khi biên dịch được gọi là sự liên kết sớm hay liên kết tĩnh) Sự liên kết muộn đồi hỏi một vài khai
báo ban đầu, nhưng cung cấp cho người lập trình sức mạnh và sự mềm dẻo
6.3 LỐP CƠ SỞ ẢO VÀ PHƯƠNG THỨC ẢO THUẦN TŨY
Hãy nhớ lại rằng, lớp Hinh trong chương trình ở ví dụ 6.L chưa hề tạo
một đối nào của lớp Hinh, mà chỉ tạo đối tượng các hình cụ thể như hình tròn, hình tam giác và hình vuông Khi không muốn có một đối tượng có kiểu thuộc lớp cơ sở, ta gọi lớp này là lớp /rừu tượng Lớp trừu tượng chỉ tồn
tại như là lớp cha của các lớp kế thừa mà chúng sẽ sử dụng để tạo các đối
tượng; nó cũng cung cấp một giao diện cho các lớp kế thừa
Trở lại ví dụ 6.1, nếu trong các lớp Tamgiac, Hinhtron hay Hinhvuong
mà không có phương thức hienthi () thì câu lệnh:
Trang 39ptrf{ i] ->hienthi();
Sẽ triệu gọi phương thức hienthi () của lớp cơ sở Thế nhưng, ta không mong muốn điều này, bởi vì ta muốn hiển thị chính xác đối tượng Vậy phải
làm cách nào? Câu trả lời là thiết lập phương thức hienthi () của lớp cơ sở Hình là phương thức ảo thuần tuý và khai báo như sau:
virtual void hienthi()const = 0;
Ví dụ 6.3 cho thấy, nếu phương thức hienthi () của lớp cơ sở là ảo
thuần tuý thì các phương thức trùng tên của các lớp kế thừa cũng là ảo và
phải có mặt trong lớp kế thừa đó Nếu lớp nào không có thì máy sẽ báo lỗi
khi thực hiện câu lệnh triệu gọi phương thức hienthi () của đối tượng gọi
phương thức này Phương thức ảo thuần tuý không làm gì cả mà chỉ là cái
Trang 40cout <<"\nDay la Hinh tron"; Me class Hinhvuong: public Hinh ¬ public: void hienthi () { cout <<"\nDay la Hinh vuong"; di void main() { Tamgiac abc; Hinhtron c; Hinhvuong hv; Hinh *ptr{ 3]; ptr[ 0) = sabc; ptrí l1]= &C; ptr[ 2] = khv;
cout<<"\nHien thi cac hinh\n"; for(int i=0; i< 3; i++)
ptr[ i] -> hienthi();
}
Kết quả:
Hien thi cac hinh Day 1a Tam giac
Day la Hinh tron
Day la Hinh vuong
Lớp Hinh được gọi là lớp trừu tượng, vì nó có phương thức ảo thuần
túy Các quy tắc sử dụng lớp trừu tượng như sau:
— Không sử đụng lớp trừu tượng để khai báo biến;
.— Lớp trừu tượng là lớp tổng quát hoá của các lớp kế thừa nó; — Ta có thể khai báo con trỏ có kiểu là một lớp trừu tượng;
— Một phương thức ảo thuần túy được khai báo trong lớp cơ sở phải được khai báo lại trong các lớp kế thừa