Skip to content Skip to navigation

OpenStax-CNX

You are here: Home » Content » Tính đa hình

Navigation

Lenses

What is a lens?

Definition of a lens

Lenses

A lens is a custom view of the content in the repository. You can think of it as a fancy kind of list that will let you see content through the eyes of organizations and people you trust.

What is in a lens?

Lens makers point to materials (modules and collections), creating a guide that includes their own comments and descriptive tags about the content.

Who can create a lens?

Any individual member, a community, or a respected organization.

What are tags? tag icon

Tags are descriptors added by lens makers to help label content, attaching a vocabulary that is meaningful in the context of the lens.

This content is ...

Affiliated with (What does "Affiliated with" mean?)

This content is either by members of the organizations listed or about topics related to the organizations listed. Click each link to see a list of all content affiliated with the organization.
  • VOCW

    This module is included inLens: Vietnam OpenCourseWare's Lens
    By: Vietnam OpenCourseWare

    Click the "VOCW" link to see all content affiliated with them.

Recently Viewed

This feature requires Javascript to be enabled.
 

Tính đa hình

Module by: Lê Thị Mỹ Hạnh. E-mail the author

Summary: Phần này trình bày về tính đa hình

DẪN NHẬP

Tính đa hình (polymorphism) là khả năng thiết kế và cài đặt các hệ thống mà có thể mở rộng dễ dàng hơn. 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, …

PHƯƠNG THỨC ẢO (VIRTUAL FUNCTION)

Khi xây dựng các lớp của một chương trình hướng đối tượng để tạo nên một cấu trúc phân cấp hoặc cây phả hệ, người lập trình phải chuẩn bị các hành vi giao tiếp chung của các lớp đó. Hành vi giao tiếp chung sẽ được dùng để thể hiện cùng một hành vi, nhưng có các hành động khác nhau, đó chính là phương thức ảo. Đây là một phương thức tồn tại để có hiệu lực nhưng không có thực trong lớp cơ sở, còn trong các lớp dẫn xuất. Như vậy phương thức ảo chỉ được xây dựng khi có một hệ thống cây phả hệ. Phương thức này sẽ được gọi thực hiện từ thực thể của lớp dẫn xuất nhưng mô tả về chúng trong lớp cơ sở.

Chúng ta khai báo phương thức ảo bằng thêm từ khóa virtual ở phía trước. Khi đó các phương thức có cùng tên với phương thức này trong các lớp dẫn xuất cũng là phương thức ảo.

Hình 1
Hình 1 (graphics1.png)
Ví dụ 6.1:

1: //Chương trình 6.1

2: #include <iostream.h>

3:

4: class Base

5: {

6: public:

7: virtual void Display()

8: {

9: cout<<"class Base"<<endl;

10: }

11: };

12:

13: class Derived : public Base

14: {

15: public:

16: virtual void Display()

17: {

18: cout<<"class Derived"<<endl;

19: }

20: };

21:

21: void Show(Base *B)

22: {

23: B->Display(); //Con trỏ B chỉ đến phương thức Display() nào (của lớp Base

24 //hoặc lớp Derived) tùy vào lúc chạy chương trình.

25: }

26: int main()

27: {

28: Base *B=new Base;

29: Derived *D=new Derived;

30: B->Display(); //Base::Display()

31: D->Display(); //Derived::Display()

32: Show(B); //Base::Display()

33: Show(D); //Derived::Display()

34: return 0;

35: }

Chúng ta chạy ví dụ 6.1, kết quả ở hình 6.1

Hình 2
Hình 2 (graphics2.png)

Hình 6.1: Kết quả của ví dụ 6.1

Trong ví dụ 6.1, lớp cơ sở Base có phương thức Display() được khai báo là phương thức ảo. Phương thức này trong lớp dẫn xuất Derived được định nghĩa lại nhưng cũng là một phương thức ảo. Thật ra, không ra không có khai báo virtual cho phương thức Display() của lớp Derived cũng chẳng sao, trình biên dịch vẫn hiểu đó là phương thức ảo. Tuy nhiên, khai báo virtual rõ ràng ở các lớp dẫn xuất làm cho chương trình trong sáng, dễ hiểu hơn. Hai dòng 30 và 31, chúng ta biết chắc phương thức Display() của lớp nào được gọi (của lớp Base hoặc lớp Derived). Nhưng hai dòng 32 và 33, nếu không có cơ chế kết nối động, chúng ta đoán rằng việc gọi hàm Show() sẽ luôn luôn kéo theo phương thức Base::Display(). Quả vậy, bỏ đi khai báo virtual cho phương thức Base::Display(), khi đó dòng lệnh: Show(D);

gọi đến Base::Display() vì đối tượng lớp dẫn xuất cũng là đối tượng lớp cơ sở (nghĩa là tdb tự động chuyển đổi kiểu: đối tượng D kiểu Derived chuyển thành kiểu Base.

Nhờ khai báo virtual cho phương thức Base::Display() nên sẽ không thực hiện gọi phương thức Base::Display() một cách cứng nhắc trong hàm Show() mà chuẩn bị một cơ chế mềm dẻo cho việc gọi phương thức Display() tùy thuộc vào sự xác định kiểu của tham số vào lúc chạy chương trình.

Cơ chế đó ra sao? Khi nhận thấy có khai báo virtual trong lớp cơ sở, trình biên dịch sẽ thêm vào mỗi đối tượng của lớp cơ sở và các lớp dẫn xuất của nó một con trỏ chỉ đến bảng phương thức ảo (virtual function table). Con trỏ đó có tên là vptr (virtual pointer). Bảng phương thức ảo là nơi chứa các con trỏ chỉ đến đoạn chương trình đã biên dịch ứng với các phương thức ảo. Mỗi lớp có một bảng phương thức ảo. Trình biên dịch chỉ lập bảng phương thức ảo khi bắt đầu có việc tạo đối tượng của lớp. Đến khi chương trình chạy, phương thức ảo của đối tượng mới được nối kết và thi hành thông qua con trỏ vptr.

Trong ví dụ 6.1, lệnh gọi hàm: Show(D);

Đối tượng D thuộc lớp Derived tuy bị chuyển đổi kiểu thành một đối tượng thuộc lớp Base nhưng nó không hoàn toàn giống một đối tượng của Base chính cống như B. Nếu như con trỏ vptr trong B chỉ đến vị trí trên bảng phương thức ảo ứng với phương thức Base::Display(), thì con trỏ vptr trong D vẫn còn chỉ đến phương thức Derived::Display() cho dù D bị chuyển kiểu thành Base. Đó là lý do tại sao lệnh: Show(D);

gọi đến phương thức Derived::Display().

    Các đặc trưng của phương thức ảo:

Hình 3
Hình 3 (graphics3.png)
Phương thức ảo không thể là các hàm thành viên tĩnh.

Hình 4
Hình 4 (graphics4.png)
Một phương thức ảo có thể được khai báo là friend trong một lớp khác nhưng các hàm friend của lớp thì không thể là phương thức ảo.

Hình 5
Hình 5 (graphics5.png)
Không cần thiết phải ghi rõ từ khóa virtual khi định nghĩa một phương thức ảo trong lớp dẫn xuất (để cũng chẳng ảnh hưởng gì).

Hình 6
Hình 6 (graphics6.png)
Để sự kết nối động được thực hiện thích hợp cho từng lớp dọc theo cây phả hệ, một khi phương thức nào đó đã được xác định là ảo, từ lớp cơ sở đến các lớp dẫn xuất đều phải định nghĩa thống nhất về tên, kiểu trả về và danh sách các tham số. Nếu đối với phương thức ảo ở lớp dẫn xuất, chúng ta lại sơ suất định nghĩa các tham số khác đi một chút thì trình biên dịch sẽ xem đó là phương thức khác. Đây chính là điều kiện để kết nối động.

Hình 7
Hình 7 (graphics7.png)
Ví dụ 6.2:

2: #include <iostream.h>

3:

4: class Base

5: {

6: public:

7: virtual void Print(int A,int B);

8: };

9:

10: class Derived : public Base

11: {

12: public:

13: virtual void Print(int A,double D);

14: };

15:

16: void Base::Print(int A,int B)

17: {

18: cout<<"A="<<A<<",B="<<B<<endl;

19: }

20:

21: void Derived::Print(int A,double D)

22: {

23: cout<<"A="<<A<<",D="<<D<<endl;

24: }

25:

26: void Show(Base *B)

27: {

28: B->Print(3,5);

29: }

30:

31: int main()

32: {

33: Base *B=new Base;

34: Derived *D=new Derived;

35: Show(B); //Base::Print()

36: Show(D); //Base::Print()

37: return 0;

38: }

Chúng ta chạy ví dụ 6.2, kết quả ở hình 6.2

Hình 8
Hình 8 (graphics8.png)

Hình 6.2: Kết quả của ví dụ 6.2

Trong ví dụ 6.2, trong lớp cơ sở Base và lớp dẫn xuất Derived đều có phương thức ảo Print(). Nhưng quan sát kỹ chúng ta, phương thức Print() trong lớp Derived có tham số thứ hai khác kiểu với phương thức Print() trong lớp Base. Vì thế, chúng ta không thể chờ đợi lệnh ở dòng 36 sẽ gọi đến phương thức Derived::Print(int,double). Phương thức Derived::Print(int,double) nằm ngoài đường dây phương thức ảo nên hàm Show() chỉ luôn gọi đến phương thức Derived::Print(int,int) mà thôi. Do có khai báo virtual đối với phương thức Derived::Print(int,double), chúng ta có thể nói phương thức này sẽ mở đầu cho một đường dây phương thức ảo Print(int,double) mới nếu sau lớp Derived còn có các lớp dẫn xuất của nó.

LỚP TRỪU TƯỢNG (ABSTRACT CLASS)

Trong quá trình thiết kế chương trình theo hướng đối tượng, để tạo nên một hệ thống phả hệ mang tính kế thừa cao, người lập trình phải đoán trước sự phát triển của cấu trúc, từ đó chọn lựa những thành viên phù hợp cho các lớp ở trên cùng. Rõ ràng đây là một công việc vô cùng khó khăn. Để tránh tình trạng người lập trình xây dựng các đối tượng lãng phí bộ nhớ, ngôn ngữ C++ cho phép chúng ta thiết kế các lớp có các không phương thức ảo không làm gì cả, và cũng không thể tạo ra đối tượng thuộc lớp đó. Những lớp như vậy gọi là lớp trừu tượng.

Trong cấu trúc trên hình 6.3, không phải lớp nào cũng thực sự cần đến phương thức Print(), nhưng nó có mặt khắp nơi để tạo ra bộ mặt chung cho mọi lớp trong cấu trúc cây. Phương thức của lớp trên cùng như A::Print() thường là phương thức ảo để có được tính đa hình.

Hình 9
Hình 9 (graphics9.png)

Hình 6.3

Nhờ đó, với hàm sau:

void Show(A* a)

{

a->Print();

}

chúng ta có thể truyền đối tượng đủ kiểu cho nó (A, B, C, D hoặc E) mà vẫn gọi đến đúng phuơng thức Print() phù hợp dù kiểu của đối tượng lúc biên dịch vẫn còn chưa biết. Với vai trò "lót đường" như vậy, phương thức A::Print() có thể chẳng có nội dung gì cả

class A

{

public:

virtual void Print()

{

}

};

Khi đó người ta gọi phương thức A::Print() là phương thức ảo rỗng (null virtual function), nó chẳng làm gì hết. Tuy nhiên lớp A vẫn là một lớp bình thường, chúng ta có thể tạo ra một đối tượng thuộc nó, có thể truy cập tới phương thức A::Print(). Để tránh trình trạng vô tình tạo ra đối tượng thuộc lớp này, người ta thường xây dựng lớp trừu tượng, trình biên dịch cho phép tạo ra lớp có các phương thức thuần ảo (pure virtual function) như sau:

class A

{

public:

virtual void Print() = 0;

};

Phương thức ảo Print() bây giờ là phương thức thuần ảo – phương thức có tên được gán bởi giá trị zero. Lớp A chứa phương thức thuần ảo được gọi là lớp trừu tượng.

Hình 10
Hình 10 (graphics10.png)
Ví dụ 6.3:

1: //Chương trình 6.3

2: #include <iostream.h>

3:

4: class A

5: {

6: public:

7: virtual void Print()=0; //Phương thức thuần ảo

8: };

9:

10: class B : public A

11: {

12: public:

13: virtual void Print()

14: {

15: cout<<"Class B"<<endl;

16: }

17: };

18:

19: class C : public B

20: {

21: public:

22: virtual void Print()

23: {

24: cout<<"Class C"<<endl;

25: }

26: };

27:

28: void Show(A *a)

29: {

30: a->Print();

31: }

32:

33: int main()

34: {

35: B *b=new B;

36: C *c=new C;

37: Show(b); //B::Print()

38: Show(c); //C::Print()

39: return 0;

40: }

Chúng ta chạy ví dụ 6.3, kết quả ở hình 6.4

Hình 11
Hình 11 (graphics11.png)

Hình 6.4: Kết quả của ví dụ 6.3

Lớp A được tạo ra để làm cơ sở cho việc hình thành các lớp con cháu (B và C). Nhờ có Phương thức ảo Print() từ lớp A cho tới lớp C, tính đa hình được thể hiện.

Hình 12
Hình 12 (graphics12.png)
Lưu ý:

Hình 13
Hình 13 (graphics13.png)
Chúng ta không thể tạo ra một đối tượng của lớp trừu tượng, nhưng hoàn toàn có thể tạo ra một con trỏ trỏ đến lớp này (vì con trỏ không phải là đối tượng thuộc lớp) hoặc là một tham chiếu.

Hình 14
Hình 14 (graphics14.png)
Nếu trong lớp kế thừa từ lớp trừu tượng chúng ta không định nghĩa phương thức thuần ảo, do tính kế thừa nó sẽ bao hàm phương thức thuần ảo của lớp cơ sở, nên lớp dẫn xuất này sẽ trở thành lớp trừu tượng.

Hình 15
Hình 15 (graphics15.png)
Theo định nghĩa lớp trừu tượng, nếu trong lớp dẫn xuất (từ lớp cơ sở trừu tượng) chúng ta định nghĩa thêm một phương thức thuần ảo khác, lớp này cũng sẽ trở thành lớp trừu tượng.

CÁC THÀNH VIÊN ẢO CỦA MỘT LỚP

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ụ 6.4: Đa năng hóa toán tử với hàm toán tử là phương thức ảo.

1: //Chương trình 6.4: Toán tử ảo

2: #include <iostream.h>

3:

4: class A

5: {

6: protected:

7: int X1;

8: public:

9: A(int I)

10: {

11: X1=I;

12: }

13: virtual A& operator + (A& T);

14: virtual A& operator = (A& T);

15: virtual int GetA()

16: {

17: return X1;

18: }

19: virtual int GetB()

20: {

21: return 0;

22: }

23: virtual int GetC()

24: {

25: return 0;

26: }

27: void Print(char *St)

28: {

29: cout<<St<<":X1="<<X1<<endl;

30: }

31: };

32:

33: class B : public A

34: {

35: protected:

36: int X2;

37: public:

38: B(int I,int J):A(I)

39: {

40: X2=J;

41: }

42: virtual A& operator + (A& T);

43: virtual A& operator = (A& T);

44: virtual int GetB()

45: {

46: return X2;

47: }

48: void Print(char *St)

49: {

50: cout<<St<<":X1="<<X1<<",X2="<<X2<<endl;

51: }

52: };

53:

54: class C : public B

55: {

56: protected:

57: int X3;

58: public:

59: C(int I,int J,int K):B(I,J)

60: {

61: X3=K;

62: }

63: virtual A& operator + (A& T);

64: virtual A& operator = (A& T);

65: virtual int GetC()

66: {

67: return X3;

68: }

69: void Print(char *St)

70: {

71: cout<<St<<":X1="<<X1<<",X2="<<X2<<",X3="<<X3<<endl;

72: }

73: };

74:

75: A& A::operator + (A& T)

76: {

77: X1+=T.GetA();

78: return *this;

79: }

80:

81: A& A::operator = (A& T)

82: {

83: X1=T.GetA();

84: return *this;

85: }

86:

87: A& B::operator + (A& T)

88: {

89: X1+=T.GetA();

90: X2+=T.GetB();

91: return *this;

92: }

93:

94: A& B::operator = (A& T)

95: {

96: X1=T.GetA();

97: X2=T.GetB();

98: return *this;

99: }

100:

101:A& C::operator + (A& T)

102: {

103: X1+=T.GetA();

104: X2+=T.GetB();

105: X3+=T.GetC();

106: return *this;

107 }

108:

109: A& C::operator = (A& T)

110: {

111: X1=T.GetA();

112: X2=T.GetB();

113: X3=T.GetC();

114: return *this;

115: }

116:

117: void AddObject(A& T1,A& T2)

118: {

119: T1=T1+T2;

120: }

121:

122: int main()

123: {

124: A a(10);

125: B b(10,20);

126: C c(10,20,30);

127: a.Print("a");

128: b.Print("b");

129: c.Print("c");

130: AddObject(a,b);

131: a.Print("a");

132: AddObject(b,c);

133: b.Print("b");

134: AddObject(c,a);

135: c.Print("c");

136: a=b+c;

137: a.Print("a");

138: c=c+a;

139: c.Print("c");

140: return 0;

141: }

Chúng ta chạy ví dụ 6.4, kết quả ở hình 6.5

Hình 16
Hình 16 (graphics16.png)

Hình 6.5: Kết quả của ví dụ 6.4

Có constructor và destructor ảo hay không?

Khi một đối tượng thuộc lớp có phương thức ả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 phương thức ảo. Một lớp chỉ có một bảng phương thức ả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 đã còn tại. Chính vì vậy bảng phương thức ảo phải được tạo ra trước khi gọi thực hiện constructor, nên constructor không thể là phương thức ảo. Ngược lại do một lớp chỉ có một bảng phương thức ảo nên khi một đối tượng thuộc lớp bị hủy bỏ, bảng phương thức ảo vẫn còn đó, và con trỏ vptr vẫn còn đó. Hơn nữa, destructor được gọi thực hiện trước khi vùng nhớ dành cho đối tượng bị thu hồi, do đó destructor có thể là phương thức ảo. Tuy nhiên, constructor của một lớp có thể gọi phương thức ảo khác. Điều này hoàn toàn không có gì mâu thuẫn với cơ chế kết nối động.

Ví dụ 6.5:

1: //Chương trình 6.5: Destructor ảo

2: #include <iostream.h>

3:

4: class Base

5: {

6: public:

7: virtual ~Base()

8: {

9: cout<<"~Base"<<endl;

10: }

11: };

12:

13: class Derived:public Base

14: {

15: public:

16: virtual ~Derived()

17: {

18: cout<<"~Derived"<<endl;

18: }

19: };

20:

21: int main()

22: {

23: Base *B;

24: B = new Derived;

25: delete B;

26: return 0;

27: }

Chúng ta chạy ví dụ 6.5, kết quả ở hình 6.6

Hình 17
Hình 17 (graphics17.png)

Hình 6.6: Kết quả của ví dụ 6.5

Nếu destructor không là phương thức ảo thì khi giải phóng đối tượng B chỉ có destructor của lớp cơ sơ được gọi mà thôi nhưng khi destructor là phương thức ảo thì khi giải phóng đối tượng B (ở dòng 25) destructor của lớp dẫn xuất được gọi thực hiện rồi đến destructor của lớp cơ sơ.

BÀI TẬP

Bài 1: Hãy xây dựng các lớp cần thiết trong phân cấp hình 5.3 để tính diện tích (hoặc diện tích xung quanh) và thể tích trong đó lớp Shape là lớp cơ sở trừu tượng.

Bài 2: Hãy sửa đổi hệ thống lương của chương trình ở ví dụ 6.6 bằng thêm các thành viên dữ liệu BrithData (một đối tượng kiểu Date) và DepartmentCode (kiểu int) vào lớp Employee. Giả sử lương này được xử lý một lần trên một tháng. Sau đó, chương trình tính bảng lương cho mỗi Employee (tính đa hình), cộng thêm 100.00$ tiền thưởng vào tổng số lương của mỗi người nếu đây là tháng mà ngày sinh của Employee xảy ra.

Bài 3: Cài đặt các lớp trong cây phả hệ lớp sau:

Hình 18
Hình 18 (graphics18.png)

Trong đó các lớp Person, Student và Staff là các lớp trừu tượng, các lớp còn lại là các lớp dẫn xuất thực.

 

Content actions

Download module as:

Add module to:

My Favorites (?)

'My Favorites' is a special kind of lens which you can use to bookmark modules and collections. 'My Favorites' can only be seen by you, and collections saved in 'My Favorites' can remember the last module you were on. You need an account to use 'My Favorites'.

| A lens I own (?)

Definition of a lens

Lenses

A lens is a custom view of the content in the repository. You can think of it as a fancy kind of list that will let you see content through the eyes of organizations and people you trust.

What is in a lens?

Lens makers point to materials (modules and collections), creating a guide that includes their own comments and descriptive tags about the content.

Who can create a lens?

Any individual member, a community, or a respected organization.

What are tags? tag icon

Tags are descriptors added by lens makers to help label content, attaching a vocabulary that is meaningful in the context of the lens.

| External bookmarks