Tải bản đầy đủ (.pdf) (36 trang)

Tương ứng bộ và phương thức ảo

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (245.88 KB, 36 trang )


191



CHƯƠNG 7
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 ñồ. 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:

192

void xuat()
{
cout << "\n Lop C " ;
}
};
Lớp C có hai lớp cơ sở tiền bối là A và B., nên lớp 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 ba phương thức xuat. Hãy theo dõi các
câu lệnh sau:
C h ; // h là ñối tượng kiểu C
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ú ý: 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 ;
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 là: Cả ba 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:
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. 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

193

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 bốn 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ả bốn lớp ñều có phương thức xuat.

#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>

class A
{
int n;
public:
A()
{
n=0;
}
A(int n1)
{
n=n1;
}
void xuat()
{
cout << "\nLop A: "<< n;
}
int getN()
{
return n;
}
};

class B: public A
{
public:

B():A() { }
B(int n1): A(n1) { }
void xuat()

194

{
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();

195

}
void main()
{
A a(1);
B b(2);
C c(3);
D d(4);
clrscr();
hien(&a);
hien(&b);
hien(&c);
hien(&d);
getch();
}
Nhận 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 trên ñây) ñều gọi tới A::xuat().
§
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:
+ nhap: Nhập dữ liệu thí sinh gồm họ tên, số báo danh, tổng ñiểm
+ xem_in: In họ tên thí sinh ra màn hình, sau ñó lựa chọn hoặc in hoặc không
+ in: In ñầy ñủ dữ liệu thí sinh ra mà hình
Chương trình dưới ñây sử dụng lớp TS (Thí sinh) ñáp ứng ñược yêu cầu ñặt ra.

196

#include <conio.h>
#include <iostream.h>
#include <ctype.h>
class TS
{
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()
{
cout<<"\nHo ten: "<<ht;
cout<<"\nSo bao danh: "<<sobd;
cout<<"\nTong diem: "<<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()

{

197

TS 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();
}
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 ta 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();
cout<<"\nDia chi: "<<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:
#include <conio.h>

198

#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class TS
{
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()
{
cout<<"\nHo ten: "<<ht;
cout<<"\nSo bao danh: "<<sobd;
cout<<"\nTong diem: "<<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

199

{
private:
char dc[30] ; // Dia chi
public:
void nhap()
{
TS::nhap();
cout << "Dia chi: " ;

fflush(stdin);
gets(dc);
}
void in()
{
TS::in();
cout<<"\nDia chi: "<<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() ;

200

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). Nhưng 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 dõ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
}
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ế nhưng 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. Do ñó 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.
ðể khắc phục ñiều này, chúng ta có thể nghĩ ñến ngay một giải pháp: xây dựng
thêm phương thức xem_in cho lớp TS2. Tuy nhiên ñiều này không chỉ làm cho
chương trình rườm rà hơn mà vẫn không thể ñáp ứng ñược yêu cầu, vì trong TS2 ta
không thể truy nhập ñược các thành phần họ tên, số báo danh, tổng ñiểm.
Trong C
++
có một giải pháp rất hiệu quả ñể xử lý vấn ñề này 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 method).
§
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à một lớp cơ sở, các lớp B, C, D dẫn xuất (trực tiếp hoặc gián tiếp) từ A.
Giả sử trong bốn 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.

201

+ 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:

202

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 (Trình biên dịch sẽ báo lỗi).
class A
{
...
virtual void hien_thi() ;

203


} ;
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.
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. 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.
Quy tắc gọi phương thức ảo:
Phương thức ảo khác phương thức tĩnh khi ñược gọi từ một con trỏ. Lời gọi tới
phương thức ảo từ một con trỏ chưa 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à bốn ñố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

204

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, nhưng 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 chưa xác ñịnh trong giai ñoạn dịch chương trình. Lời gọi này sẽ:

×