Chơng 6
Tơng ứng bội và phơng thức ảo
Tơng ứng bội và phơng thức ảo là công cụ mạnh của C++
cho phép tổ chức quản lý các đối tợng khác nhau theo cùng một lợc
đồ. Một khái niệm khác liên quan là: lớp cơ sở trừu tợng. Chơng này
sẽ trình bầy cách sử dụng các công cụ trên để 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.
Đ
1. Phơng thức tĩnh
1.1. Lời gọi tới phơng thức tĩnh
Nh đã biết một lớp dẫn xuất đợc thừa kế các phơng thức của các
lớp cơ sở tiền bối của nó. Ví dụ lớp A là cơ sở của B, lớp B lại là cơ
sở của C, thì C có 2 lớp cơ sở tiền bối là B và A. Lớp C đợc thừa kế
các phơng thức của A và B. Các phơng thức mà chúng ta vẫn nói là
các phơng thức tĩnh. Để tìm hiểu thêm về cách gọi tới các phơng thức
tĩnh, ta xét ví dụ về các lớp A, B và C nh sau:
class A
{
public:
void xuat()
{
cout << "\n Lop A " ;
}
};
class B:public A
{
public:
void xuat()
{
cout << "\n Lop B " ;
}
};
class C:public B
{
public:
void xuat()
{
cout << "\n Lop C " ;
}
};
Lớp C có 2 lớp cơ sở tiền bối là A , B và C kế thừa các phơng thức
của A và B. Do đó một đối tợng của C sẽ có tới 3 phơng thức xuat.
Hãy theo rõi các câu lệnh sau:
C h ; // h là đối tợng kiểu C
h.xuat() ; // Gọi tới phơng thức h.D::xuat()
h.B::xuat() ; // Gọi tới phơng thức h.B::xuat()
h.A::xuat() ; // Gọi tới phơng thức h.A::xuat()
Các lời gọi phơng thức trong ví dụ trên đều xuất phát từ đối tợng h
và mọi lời gọi đều xác định rõ phơng thức cần gọi.
Bây giờ chúng ta hãy xét các lời gọi không phải từ một biến đối t-
ợng mà từ một con trỏ. Xét các câu lệnh:
A *p, *q, *r; // p, q, r là con trỏ kiểu A
A a; // a là đối tợng kiểu A
B b; // b là đối tợng kiểu B
C c; // c là đối tợng kiểu c
Chúng ta hãy ghi nhớ mệnh đề sau về con trỏ của các lớp dẫn
xuất và cơ sở:
317 318
Phép gán con trỏ: Con trỏ của lớp cơ sở có thể dùng để chứa địa
chỉ các đối tợng của lớp dẫn xuất.
Nh vậy cả 3 phép gán sau đều hợp lệ:
p = &a ;
q = &b ;
r = &c ;
Chúng ta tiếp tục xét các lời gọi phơng thức từ các con trỏ p, q, r:
p->xuat();
q->xuat();
r->xuat();
và hãy lý giải xem phơng thức nào (trong các phơng thức A::xuat,
B::xuat và C::xuat) đợc gọi. Câu trả lời nh sau:
Cả 3 câu lệnh trên đều gọi tới phơng thức A::xuat() , vì các con trỏ
p, q và r đều có kiểu A.
Nh vậy có thể tóm lợc cách thức gọi các phơng thức tĩnh nh sau:
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ệ thừa kế) đợc gọi:
1. 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.
2. 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.
1.2. Ví dụ
Xét 4 lớp A, B, C và D. Lớp B và C có chung lớp cơ sở A. Lớp D
dẫn xuất từ C. Cả 4 lớp đều có phơng thức xuat(). Xét hàm:
void hien(A *p)
{
p->xuat();
}
Không cần biết tới địa chỉ của đối tợng nào sẽ truyền cho đối con
trỏ p, lời gọi trong hàm luôn luôn gọi tới phơng thức A::xuat() vì con
trỏ p kiểu A. Nh vậy bốn câu lệnh:
hien(&a);
hien(&b);
hien(&c);
hien(&d);
trong hàm main (của chơng trình dới đây) đều gọi tới A::xuat().
//CT6-01
// Phuong thuc tinh
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class A
{
private:
int n;
public:
A()
{
n=0;
}
A(int n1)
{
n=n1;
}
void xuat()
{
cout << "\nLop A: "<< n;
319 320
}
int getN()
{
return n;
}
};
class B:public A
{
public:
B():A()
{
}
B(int n1):A(n1)
{
}
void xuat()
{
cout << "\nLop B: "<<getN();
}
};
class C:public A
{
public:
C():A()
{
}
C(int n1):A(n1)
{
}
void xuat()
{
cout << "\nLop C: "<<getN();
}
};
class D:public C
{
public:
D():C()
{
}
D(int n1):C(n1)
{
}
void xuat()
{
cout << "\nLop D: "<<getN();
}
};
void hien(A *p)
{
p->xuat();
}
void main()
{
A a(1);
B b(2);
C c(3);
D d(4);
clrscr();
321 322
hien(&a);
hien(&b);
hien(&c);
hien(&d);
getch();
}
Đ
2. Sự hạn chế của phơng thức tĩnh
Ví dụ sau cho thấy sự hạn chế của phơng thức tĩnh trong việc sử
dụng tính thừa kế để phát triển chơng trình.
Giả sử cần xây dựng chơng trình quản lý thí sinh. Mỗi thí sinh đa
vào ba thuộc tính: Họ tên, số báo danh và tổng điểm. Chơng trình
gồm ba chức năng: Nhập dữ liệu thí sinh, in dữ liệu thí sinh ra máy in
và xem - in (in họ tên ra màn hình, sau đó lựa chọn hoặc in hoặc
không). Chơng trình dới đây sử dụng lớp TS (Thí sinh) đáp ứng đợc
yêu cầu đặt ra.
//CT6-02
// Han che phuong thuc tinh
// Lop TS
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class TS
{
private:
char ht[25];
int sobd;
float td;
public:
void nhap()
{
cout << "\nHo ten: " ;
fflush(stdin); gets(ht);
cout << "So bao danh: " ;
cin >> sobd;
cout << "Tong diem: " ;
cin >> td;
}
void in()
{
fprintf(stdprn,"\n\nHo ten: %s", ht);
fprintf(stdprn,"\nSo bao danh: %d", sobd);
fprintf(stdprn,"\nTong diem: %0.1f", td);
}
void xem_in()
{
int ch;
cout << "\nHo ten: " << ht ;
cout << "\nCo in khong? - C/K" ;
ch = toupper(getch());
if (ch=='C')
this->in();
}
} ;
void main()
{
TS t[100];
int i, n;
cout << "\nSo thi sinh: ";
323 324
cin >> n;
for (i=1; i<=n; ++i)
t[i].nhap();
for (i=1; i<=n; ++i)
t[i].xem_in();
getch();
}
Giả sử Nhà trờng muốn quản lý thêm địa chỉ của thí sinh. Vì sự
thay đổi ở đây là không nhiều, nên chúng ta không đả động đến lớp
TS mà xây dựng lớp mới TS2 dẫn xuất từ lớp TS. Trong lớp TS2 đa
thêm thuộc tính dc (địa chỉ) và các phơng thức nhap, in. Cụ thể lớp
TS2 đợc định nghĩa nh sau:
class TS2:public TS
{
private:
char dc[30] ; // Dia chi
public:
void nhap()
{
TS::nhap();
cout << "Dia chi: " ;
fflush(stdin); gets(dc);
}
void in()
{
TS::in();
fprintf(stdprn,"\nDia chi: %s", dc);
}
};
Trong lớp TS2 không xây dựng lại phơng thức xem_in, mà sẽ
dùng phơng thức xem_in của lớp TS. Chơng trình mới nh sau:
//CT6-03
// Han che phuong thuc tinh
// Lop TS TS2
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class TS
{
private:
char ht[25];
int sobd;
float td;
public:
void nhap()
{
cout << "\nHo ten: " ;
fflush(stdin); gets(ht);
cout << "So bao danh: " ;
cin >> sobd;
cout << "Tong diem: " ;
cin >> td;
}
void in()
{
fprintf(stdprn,"\n\nHo ten: %s", ht);
fprintf(stdprn,"\nSo bao danh: %d", sobd);
325 326
fprintf(stdprn,"\nTong diem: %0.1f", td);
}
void xem_in()
{
int ch;
cout << "\nHo ten: " << ht ;
cout << "\nCo in khong? - C/K" ;
ch = toupper(getch());
if (ch=='C')
this->in(); //Goi den TS::in() (Vi this la con tro
//kieu TS)
}
} ;
class TS2:public TS
{
private:
char dc[30] ; // Dia chi
public:
void nhap()
{
TS::nhap();
cout << "Dia chi: " ;
fflush(stdin); gets(dc);
}
void in()
{
TS::in();
fprintf(stdprn,"\nDia chi: %s", dc);
}
};
void main()
{
TS2 t[100];
int i, n;
cout << "\nSo thi sinh: ";
cin >> n;
for (i=1; i<=n; ++i)
t[i].nhap();
for (i=1; i<=n; ++i)
t[i].xem_in();
getch();
}
Khi thực hiện chơng trình này, chúng ta nhận thấy: Dữ liệu in ra
vẫn không có địa chỉ. Điều này có thể giải thích nh sau:
Xét câu lệnh (thứ 2 từ dới lên trong hàm main):
t[i].xem_in() ;
Câu lệnh này gọi tới phơng thức xem_in của lớp TS2 (vì t[i] là đối
tợng của lớp TS2). Nhng lớp TS2 không định nghĩa phơng thức
xem_in, nên phơng thức TS::xem_in() sẽ đợc gọi tới. Hãy theo rõi
phơng thức này:
void xem_in()
{
int ch;
cout << "\nHo ten: " << ht ;
cout << "\nCo in khong? - C/K" ;
ch = toupper(getch());
if(ch=='C')
this->in(); //Goi den TS::in() (Vi this la con tro kieu TS)
327 328
}
Các lệnh đầu của phơng thức sẽ in họ tên thí sinh. Nếu chọn có
(bấm phím C), thì câu lệnh:
this->in() ;
sẽ đợc thực hiện. Mặc dù địa chỉ của t[i] (là đối tợng của lớp TS2) đ-
ợc truyền cho con trỏ this, thế nhng câu lệnh này luôn luôn gọi tới
phơng thức TS::in(), vì con trỏ this ở đây có kiểu TS và vì in() là ph-
ơng thức tĩnh. Kết quả là không in đợc địa chỉ của thí sinh.
Nh vậy việc sử dụng các phơng thức tĩnh in() (trong các lớp TS và
TS2) đã không đáp ứng đợc yêu cầu phát triển chơng trình. Có một
giải pháp rất đơn giản là: Định nghĩa các phơng thức in() trong các
lớp TS và TS2 nh các phơng thức ảo (virtual).
Đ
3. Phơng thức ảo và tơng ứng bội
3.1. Cách định nghĩa phơng thức ảo
Giả sử A là lớp cơ sở, các lớp B, C, D dẫn xuất (trực tiếp hoặc dán
tiếp) từ A. Giả sử trong 4 lớp trên đều có các phơng thức trùng dòng
tiêu đề (trùng kiểu, trùng tên, trùng các đối). Để định nghĩa các ph-
ơng thức này là các phơng thức ảo, ta chỉ cần:
+ Hoặc thêm từ khoá virtual vào dòng tiêu đề của phơng thức bên
trong định nghĩa lớp cơ sở A.
+ Hoặc thêm từ khoá virtual vào dòng tiêu đề bên trong định
nghĩa của tất cả các lớp A, B, C và D.
Ví dụ:
Cách 1:
class A
{
...
virtual void hien_thi()
{
cout << \n Đây là lớp A ;
};
} ;
class B : public A
{
...
void hien_thi()
{
cout << \n Đây là lớp B ;
};
} ;
class C : public B
{
...
void hien_thi()
{
cout << \n Đây là lớp C ;
};
} ;
class D : public A
{
...
void hien_thi()
{
cout << \n Đây là lớp D ;
};
} ;
Cách 2:
329 330
class A
{
...
virtual void hien_thi()
{
cout << \n Đây là lớp A ;
};
} ;
class B : public A
{
...
virtual void hien_thi()
{
cout << \n Đây là lớp B ;
};
} ;
class C : public B
{
...
virtual void hien_thi()
{
cout << \n Đây là lớp C ;
};
} ;
class D : public A
{
...
virtual void hien_thi()
{
cout << \n Đây là lớp D ;
};
} ;
Chú ý: Từ khoá virtual không đợc đặt bên ngoài định nghĩa lớp.
Ví dụ nếu viết nh sau là sai (CTBD sẽ báo lỗi).
class A
{
...
virtual void hien_thi() ;
} ;
virtual void hien_thi() // Sai
{
cout << \n Đây là lớp A ;
};
Cần sửa lại nh sau:
class A
{
...
virtual void hien_thi() ;
} ;
void hien_thi() // Đúng
{
cout << \n Đây là lớp A ;
};
3.2. Quy tắc gọi phơng thức ảo
Để có sự so sánh với phơng thức tĩnh, ta nhắc lại quy tắc gọi ph-
ơng thức tĩnh nêu trong
Đ
1.
3.2.1. 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ệ thừa
kế) đợc gọi:
331 332
1. 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.
2. 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.
3.2.2. Quy tắc gọi phơng thức ảo
Phơng thức ảo chỉ khác phơng thức tĩnh khi đợc gọi từ một con trỏ
(trờng hợp 2 nêu trong mục 3.2.1). Lời gọi tới phơng thức ảo từ một
con trỏ cha cho biết rõ phơng thức nào (trong số các phơng thức ảo
trùng tên của các lớp có quan hệ thừa kế) 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.
Ví dụ A, B, C và D là các lớp đã định nghĩa trong 3.1. Ta khai báo
một con trỏ kiểu A và 4 đối tợng:
A *p ; // p là con trỏ kiểu A
A a ; // a là biến đối tợng kiểu A
B b ; // b là biến đối tợng kiểu B
C c ; // c là biến đối tợng kiểu C
D d ; // d là biến đối tợng kiểu D
Xét lời gọi tới các phơng thức ảo hien_thi sau:
p = &a; // p trỏ tới đối tợng a của lớp A
p->hien_thi() ; // Gọi tới A::hien_thi()
p = &b; // p trỏ tới đối tợng b của lớp B
p->hien_thi() ; // Gọi tới B::hien_thi()
p = &c; // p trỏ tới đối tợng c của lớp C
p->hien_thi() ; // Gọi tới C::hien_thi()
p = &d; // p trỏ tới đối tợng d của lớp D
p->hien_thi() ; // Gọi tới D::hien_thi()
3.3. Tơng ứng bội
Chúng ta nhận thấy cùng một câu lệnh
p->hien_thi();
tơng ứng với nhiều phơng thức khác nhau. Đây chính là tơng ứng bội.
Khả năng này rõ ràng cho phép xử lý nhiều đối tợng khác nhau,
nhiều công việc, thậm chí nhiều thuật toán khác nhau theo cùng một
cách thức, cùng một lợc đồ. Điều này sẽ đợc minh hoạ trong các mục
tiếp theo.
3.4. Liên kết động
Có thể so sánh sự khác nhau giữ phơng thức tĩnh và phơng thức ảo
trên khía cạnh liên kết một lời gọi với một phơng thức. Trở lại ví dụ
trong 3.2:
A *p ; // p là con trỏ kiểu A
A a ; // a là biến đối tợng kiểu A
B b ; // b là biến đối tợng kiểu B
C c ; // c là biến đối tợng kiểu C
D d ; // d là biến đối tợng kiểu D
Nếu hien_thi() là các phơng thức tĩnh, thì dù p chứa địa chỉ của
các đối tợng a, b, c hay d, thì lời gọi:
p->hien_thi() ;
luôn luôn gọi tới phơng thức A::hien_thi()
Nh vậy một lời gọi (xuất phát từ con trỏ) tới phơng thức tĩnh luôn
luôn liên kết với một phơng thức cố định và sự liên kết này xác định
trong quá trình biên dịch chơng trình.
Cũng với lời gọi:
p->hien_thi() ;
nh trên, nhng nếu hien_thi() là các phơng thức ảo, thì lời gọi này
không liên kết cứng với một phơng thức cụ thể nào. Phơng thức mà
nó liên kết (gọi tới) còn cha xác định trong giai đoạn dịch chơng
trình. Lời gọi này sẽ:
+ liên kết với A::hien_thi() , nếu p chứa địa chỉ đối tợng lớp A
333 334