Chuong 6
CHƯƠNG 6: TÍNH KẾ THỪA VÀ TƯƠNG ỨNG BỘI
GIỚI THIỆU
Hai khái niệm rất quan trọng trong lập trình hướng đối tượng là tính kế thừa (inheritance) và tương ứng bội (polymorphism). Tính kế thừa cho phép xây dựng các lớp mới dựa trên các lớp đã có. Tính tương ứng bội cho phép xử lý các đối tượng có liên hệ với nhau theo một cách thức chung. Chương này cung cấp cho sinh viên các kiến thức sau:
- Khái niệm kế thừa, truy nhập các thành phần trong kế thừa, đa kế thừa
- Các lớp cơ sở ảo
- Khái niệm thành phần ảo và tương ứng bội, các lớp cơ sở trừu tượng
6.1. KHÁI NIỆM KẾ THỪA.
Một lớp kế thừa (sử dụng lại cấu trúc biến thành phần và hàm thành phần) từ một lớp khác được gọi là lớp dẫn xuất (derived class). Lớp mà từ nó đã kế thừa được gọi là lớp cơ sở (base class). Lớp nào cũng có thể là lớp cơ sở. Một lớp có thể là cơ sở cho nhiều lớp dẫn xuất khác nhau. Tiếp theo, một lớp dẫn xuất có thể là cơ sở cho một số lớp khác. Một lớp dẫn xuất phải liệt kê tên các lớp cơ sở của nó trong khai báo.
Giả sử đã khai báo các lớp A và B. Lớp C dẫn xuất xây dựng từ lớp A và B như sau:
class C: public A, private B, protected C
{ private:
//Khai_báo_các_biến
//Khai_báo_các_hàm
protected:
//Khai_báo_các_biến
//Khai_báo_các_hàm
public:
//Khai_báo_các_biến
//Khai_báo_các_hàm
};
Ví dụ:
#include <iostream.h>
class B
{ private:
int a;
public:
int b;
void get_ab();
int tell_a(void);
void show_a(void);
};
class D: public B
{ private:
int c;
public:
void mul(void);
void display(void);
};
void B::get_ab(void)
{ a=5; b=10; }
int B::tell_a(void)
{ return a; }
void B::show_a(void)
{ cout <<"a= "<<a<<"
"; }
void D::mul()
{ c=b*tell_a(); }
void D::display()
{ cout<<"a= "<<tell_a()<<"
";
cout<<"b= "<<b<<"
";
cout<<"c= "<<c<<"
";
}
int main()
{ D d;
d.get_ab();
d.mul();
d.show_a();
d.display();
d.b=20;
d.mul();
d.display();
return 0;
}
Việc ghi thêm từ khoá public B vào trong định nghĩa của D chỉ ra D là lớp dẫn xuất của B. Từ khoá public cho phép lớp D kế thừa những thành phần public trong B. Những thành phần private của B không được kế thừa.
6.2 HÀM KHỞI TẠO VÀ HÀM HUỶ TRONG KẾ THỪA
6.2.1 Hàm khởi tạo với tính kế thừa
Lớp dẫn xuất không thể kế thừa các hàm khởi tạo và huỷ bỏ của lớp cơ sở. Thay vào đó, các hàm khởi tạo cho lớp dẫn xuất phải chứa đựng những thông tin làm đối số cho các hàm khởi tạo của lớp cơ sở.
Ví dụ:
#include <conio.h>
#include <iostream.h>
#include <string.h>
class A
{ private:
int a;
char *str;
public:
A() { a=0; str=NULL; }
A(int a1,char *str1) { a=a1; str=strdup(str1); }
void display()
{ cout<<"
Số nguyên lớpMsoNormal"> <<"
Chuỗi lớp A:"<<str;
}
};
class B
{ private:
int b;
char *str;
public:
B() { b=0; str=NULL; }
B(int b1,char *str1)
{ b=b1; str=strdup(str1); }
void display()
{ cout<<"
Số nguyên lớpMsoNormal"> <<"
Chuỗi lớp B:"<<str;
}
};
class C: public B
{ private:
int c;
char *str;
public:
C():B() { c=0; str=NULL; }
C(int b1,char *strb,int c1,char *strc):B(b1,strb)
{ c=c1; str=strdup(strc);}
void display()
{ B::display();
cout<<"
Số nguyên lớpMsoNormal"> <<"
Chuỗi lớp C:"<<str;
}
};
class D: public C
{ private:
int d;
char *str;
A u;
public:
D():C(),u()
{ d=0; str=NULL; }
D(int a1,char *stra,int b1,char *strb,int c1,char *strc,
int d1, char *strd):u(a1,stra),C(b1,strb,c1,strc)
{ d=d1; str=strdup(strd); }
void display()
{ u.display();
C::display();
cout<<"
Số nguyên lớpMsoNormal"> <<"
Chuỗi lớp D:"<<str;
}
};
int main()
{
D h(1,"AA",2,"BB",3,"CC",4,"DD");
clrscr();
cout<<"
Các thuộc tính của h thừa kế B: ";
h.B::display();
cout<<"
Các thuộc tính của h thừa kế B và C: ";
h.C::display();
cout<<"
Các thuộc tính của h thừa kế B, C và khai báo trong D: ";
h.display();
getch();
return 0;
}
Trong ví dụ trên, định nghĩa hàm khởi tạo của lớp D phải gửi các giá trị cho hàm khởi tạo của lớp C. Định nghĩa hàm khởi tạo lớp D như sau:
D():C(),u()
{ d=0; str=NULL; }
D(int a1,char *stra,int b1,char *strb,int c1,char *strc,
int d1, char *strd):u(a1,stra),C(b1,strb,c1,strc)
{ d=d1; str=strdup(strd); }
Việc gọi đến một hàm khởi tạo của lớp cơ sở cũng giống như gọi đến các hàm khởi tạo cho những thành phần có kiểu đối tượng. Phải cho tên lớp cơ sở và sau đó là danh sách đối số cho hàm khởi tạo này.
Lớp D dẫn xuất từ lớp C và chứa thành phần u, thành phần này là một đối tượng của lớp A. Hàm khởi tạo cho lớp D phải có một danh sách khởi động cung cấp đối số của hàm khởi tạo cho cả lớp cơ sở lẫn đối tượng thành phần. Hàm khởi tạo cho lớp cơ sở thì được phân biệt bằng tên của nó. Hàm khởi tạo cho thành phần có kiểu đối tượng được phân biệt nhờ tên của đối tượng.
6.2.2 Hàm huỷ bỏ đối tượng với tính kế thừa
Khi một đối tượng của lớp dẫn xuất bị huỷ bỏ, thì các đối tượng thành phần và các đối tượng thừa kế từ các lớp cơ sở cũng bị giải phóng theo. Do đó các hàm huỷ bỏ tương ứng sẽ được gọi đến.
Như vậy khi định nghĩa các hàm huỷ bỏ của lớp dẫn xuất, chúng ta chỉ cần quan tâm đến các biến thuộc tính khai báo thêm trong lớp dẫn xuất mà thôi. Chúng ta không cần quan tâm đến các đối tượng thành phần và các thuộc tính thừa kế từ lớp cơ sở.
Ví dụ: Sử dụng hàm khởi tạo và hàm huỷ bỏ trong chương trình quản lý giáo viên và môn học
#include <conio.h>
#include <iostream.h>
#include <string.h>
class MON_HOC
{ private:
char *monhoc;
int st;
public:
MON_HOC()
{ monhoc=NULL; st=0; }
MON_HOC(char *monhoc1, int st1)
{ int n=strlen(monhoc1);
monhoc=new char[n+1];
strcpy(monhoc,monhoc1);
st=st1;
}
~MON_HOC()
{ if (monhoc!=NULL)
{ delete monhoc; st=0; }
}
void in()
{ cout <<"
Tên môn: "<<monhoc;
cout <<"
Số tiết: "<<st;
}
};
class NHAN_SU
{ private:
char *ht;
int ns;
public:
NHAN_SU()
{ ht=NULL; ns=0; }
NHAN_SU(char *ht1,int ns1)
{ int n=strlen(ht1);
ht=new char[n+1];
strcpy(ht,ht1);
ns=ns1;
}
~ NHAN_SU()
{ if (ht!=NULL)
{ delete ht; ns=0; }
}
void in()
{ cout <<"
Họ tên: "<<ht;
cout <<"
Năm sinh: "<<ns;
}
};
class GIAO_VIEN: public NHAN_SU
{ private:
char *bomon;
MON_HOC mh;
public:
GIAO_VIEN():mh(),NHAN_SU()
{ bomon=NULL; }
GIAO_VIEN(char *ht1,int ns1,char *monhoc1,int st1,
char *bomon1): NHAN_SU(ht1,ns1),mh(monhoc1,st1)
{ int n=strlen(bomon1);
bomon=new char[n+1];
strcpy(bomon, bomon1);
}
~GIAO_VIEN()
{ if (bomon!=NULL) delete bomon; }
void in()
{ NHAN_SU::in();
cout <<"
Công tác tại bộ môn: "<<bomon;
mh.in();
}
};
int main()
{ clrscr();
GIAO_VIEN g1;
GIAO_VIEN *g2;
g2=new GIAO_VIEN("LE TUAN",1960,"LTHDT",75,"CNPM");
g2->in(); // hoặc g2->GIAO_VIEN::in();
g2->NHAN_SU::in(); getch();
delete g2; getch();
return 0;
}
6.3. TRUY NHẬP TỚI CÁC THÀNH PHẦN TRONG KẾ THỪA LỚP
Nếu một lớp dẫn xuất khai báo một lớp cơ sở là public thì mọi thành phần public của lớp cơ sở này trở thành những thành phần public của lớp dẫn xuất. Các thành phần protected của lớp cơ sở trở thành thành phần protected của lớp dẫn xuất. Các thành phần private của lớp cơ sở không được thừa kế
Nếu một lớp dẫn xuất khai báo một lớp cơ sở là private, thì mọi thành phần public của lớp cơ sở sẽ trở thành những thành phần private của lớp dẫn xuất. Các thành phần protected của lớp cơ sở trở thành thành phần private của lớp dẫn xuất. Các thành phần private của lớp cơ sở không được thừa kế
Nếu một lớp dẫn xuất khai báo một lớp cơ sở là protected, thì mọi thành phần public của lớp cơ sở sẽ trở thành những thành protected của lớp dẫn xuất. Các thành phần protected của lớp cơ sở trở thành thành phần protected của lớp dẫn xuất. Các thành phần private của lớp cơ sở không được thừa kế
Ví dụ:
#include <iostream.h>
class base
{ private:
int base_no;
public:
void set(int i) {base_no=i;}
void print() { cout << base_no; }
};
class derived1: public base
{ public:
void print1() { cout << base_no; }
};
class derived2: private base
{ ........................ };
main()
{ derived1 d1;
derived2 d2;
d1.set(1); d1.print();
d2.set(2); d2.print();
}
Trong ví dụ trên, hai dòng lệnh cuối:
d2.set(2);
và d2.print();
là sai vì set() và print() là hàm private của lớp derived2 nên không thể gọi tới từ hàm main(). Định nghĩa print1() trong derived1 là không đúng vì base_no là thành phần private của base.
Không bắt buộc phải luôn luôn có từ chỉ định truy xuất cho lớp cơ sở. Nếu không có từ chỉ định, lớp cơ sở được định nghĩa với từ khóa class được xem là private, còn lớp cơ sở được khai báo với từ khoá struct sẽ được xem như là public.
Sự tham khảo và truy xuất có bảo vệ (protected)
Trên thực tế, có thể xảy ra trường hợp cần một lớp dẫn xuất để truy xuất các thành phần của một lớp cơ sở, nhưng không cần phải khai báo các thành phần của lớp cơ sở đó là public. Khi đó có thể dùng chỉ định protected trong định nghĩa lớp cơ sở. Các thành phần định nghĩa trong phạm vi protected của lớp cơ sở được xem như public bên trong phạm vi của lớp dẫn xuất (chúng vẫn được xem như private đối với mọi lớp khác).
Ví dụ:
#include <iostream.h>
class base
{ protected:
int base_no;
public:
void set(int i) {base_no=i;}
void print() { cout << base_no; }
};
class derived1: public base
{ public:
void print1() { cout << base_no; }
};
int main()
{ base b;
derived1 d;
d.set(1); d.print();
b.set(2);
int i=base_no;
return 0;
}
Dòng lệnh
int i=base_no;
là sai vì base_no là thành phần protected trong lớp base. derived1 có thể truy xuất vào thành phần base_no được bảo vệ của lớp cơ sở của nó. Phần còn lại của chương trình vẫn xem base_no như là thành phần private của base. Do đó, hàm main() không truy nhập được vào base_no.
Một thành phần trong phạm vi protected của một lớp cơ sở public được xem như là thành phần protected trong lớp dẫn xuất. Nếu lớp cơ sở được khai báo là private thì các thành phần protected của nó trở thành private trong lớp dẫn xuất.
Chỉ định truy xuất protected cho phép linh hoạt hơn trong việc kiểm soát cách thức truy xuất các phần tử của lớp khi có kế thừa. Các thành phần được khai báo private thì được bảo vệ tránh những truy xuất từ bên ngoài phạm vi lớp. Các thành phần khai báo protected ngăn cản các truy xuất từ bên ngoài phạm vi của lớp đó hoặc mọi lớp dẫn xuất từ nó. Các thành phần public thì có thể truy xuất ở bất kỳ phạm vi nào.
Các thành phần public và protected của lớp cơ sở sẽ trở thành các thành phần public và protected của lớp dẫn xuất theo kiểu public. Các thành phần public và protected của lớp cơ sở sẽ trở thành các thành phần private của lớp dẫn xuất theo kiểu private.
Ví dụ: Chương trình kiểm tra truy xuất tới các thành phần lớp
#include <conio.h>
#include <iostream.h>
#include <string.h>
class A
{ protected:
int a1;
public:
int a2;
A() { a1=a2=0; }
A(int t1, int t2) { a1=t1; a2=t2; }
void in() { cout<<a1<<" "<<a2; }
};
class B: private A
{ protected:
int b1;
public:
int b2;
B() { b1=b2=0; }
B(int t1, int t2, int u1, int u2)
{ a1=t1; a2=t2; b1=u1; b2=u2; }
void in()
{ cout<<a1<<" "<<a2<<" ";
cout<<b1<<" "<<b2;
}
};
class C: public B
{ public:
C() { b1=b2=0; }
C(int t1, int t2, int u1, int u2)
{ a1=t1; a2=t2; b1=u1; b2=u2; }
void in()
{ cout<<a1<<" "<<a2<<" ";
cout<<b1<<" "<<b2;
}
};
int main()
{ C c(1,2,3,4);
c.in();
getch();
return 0;
}
Khi chạy chương trình trên, ta nhận được 4 thông báo lỗi sau:
A::a1 is not accessible
A::a2 is not accessible
A::a1 is not accessible
A::a2 is not accessible
Nếu sửa đổi để lớp B dẫn xuất public từ lớp A thì chương trình không có lỗi và thực hiện tốt.
6.4 ĐA KẾ THỪA (MULIPLE INHERITANCE)
6.4.1 Xây dựng lớp dẫn xuất kế thừa nhiều lớp
Một lớp có thể có nhiều lớp cơ sở. Điều này được gọi là kế thừa nhiều lớp vì lớp dẫn xuất được kế thừa từ nhiều lớp cơ sở.
Ví dụ:
class base1
{ protected:
int b1;
public:
void set(int i) {b1=i;}
};
class base2
{ private:
int b2;
public:
void set(int i) {b2=i;}
int get() { return b2;}
};
class derived: public base1, private base2
{ public:
void print() { cout <<b1<<get(); }
};
Lớp derived kế thừa từ hai lớp base1 và base2 (base1 được khai báo là lớp cơ sở public, base2 được khai báo là lớp cơ sở private). Do đó, lớp derived có thể truy xuất trực tiếp các thành phần dữ liệu public và protected của base1, nhưng nó chỉ có thể truy xuất các thành phần public trong base2. Các thành phần public của base1 có thể được truy xuất như các thành phần public của derived1, nhưng không có những thành phần nào kế thừa từ base2 có thể được truy xuất từ bên ngoài phạm vi của derived1.
6.4.2 Sử dụng các thành phần trong lớp dẫn xuất kế thừa nhiều lớp
Thành phần của lớp dẫn xuất bao gồm:
- Các thành phần khai báo trong lớp dẫn xuất
- Các thành phần mà lớp dẫn xuất thừa kế từ các lớp cơ sở.
Khi sử dụng các thành phần trong lớp dẫn xuất, có thể dùng một trong hai cách sau:
Dùng tên lớp và tên thành phần.
Ví dụ:
D h; //h là đối tượng của lớp D dẫn xuất từ A và B
h.D::n là thuộc tính n khai báo trong D
h.A::n là thuộc tính n thừa kế từ A (khai báo trong A)
h.D::nhap() là hàm nhap() định nghĩa trong D
h.A::nhap() là hàm nhap() định nghĩa trong A
Không dùng tên lớp, chỉ dùng tên thành phần.
Khi đó, chương trình dịch C++ phải tự xác định thành phần đó thuộc lớp nào: Trước tiên xem thành phần đang xét có trùng tên với một thành phần nào của lớp dẫn xuất không. Nếu trùng thì đó là thành phần của lớp dẫn xuất. Nếu không trùng thì tiếp tục xét các lớp cơ sở theo thứ tự. Các lớp có quan hệ gần với lớp dẫn xuất xét trước, các lớp có quan hệ xa xét sau.
Ví dụ:
class A
{ private:
int n;
float a[20];
public:
void nhap();
void xuat();
};
class B
{ private:
int m, n;
float a[20][20];
public:
void nhap();
void xuat();
};
class C: public A, public B
{ private:
int k;
public:
void nhap();
void xuat();
};
Dùng các hàm của lớp A, B để định nghĩa các hàm lớp D
void D::nhap()
{ cout<< "
Nhập k: "; cin >>k;
A::nhap(); B::nhap();
}
void D::xuat()
{ cout<< "
k= "<<k;
cin >>k;
A::xuat();
B::xuat();
}
Làm việc với các đối tượng của lớp dẫn xuất
D h; //h là đối tượng của lớp D
h.nhap(); //tương đương h.D::nhap()
h.A::xuat(); // In các giá trị h.A::n và h.A::a
h.B::xuat(); // In các giá trị h.B::m, h.B::n và h.B::a
h.D::xuat(); // In tất cả các giá trị của h
h.xuat(); //tương đương h.D::xuat()
6.5 CÁC LỚP CƠ SỞ ẢO (VIRTUAL BASE CLASSES)
Cùng một lớp không thể khai báo hai lần trong danh sách các lớp cơ sở cho một lớp dẫn xuất. Tuy nhiên vẫn có thể có trường hợp cùng một lớp cơ sở được đề cập nhiều hơn một lần trong các lớp cơ sở tổ tiên của một lớp dẫn xuất. Điều này có thể phát sinh lỗi vì không có cách nào để phân biệt hai lớp cơ sở gốc.
Ví dụ:
#include <iostream.h>
class A
{ public:
int v1;
};
class B: public A
{ public:
double v2;
};
class C: public A
{ public:
float v3;
};
class D: public B, public C
{ public:
char v4;
};
int main()
{ D obj;
obj.v4='a';
obj.v3=3.14;
obj.v2=1.5;
obj.v1=0;
cout<<"
"<< obj.v4;
cout<<"
"<< obj.v3;
cout<<"
"<< obj.v2;
cout<<"
"<< obj.v1;
return 0;
}
Trong ví dụ trên, thành phần v1 của obj trong hàm main() là thành phần của lớp cơ sở A, mà A là lớp cơ sở cho cả hai lớp cơ sở của D. Khi đó, chương trình biên dịch không thể nhận biết phép gán cho v1 được kế thừa thông qua B hay cho v1 kế thừa thông qua C.
Để giải quyết vấn đề này, chúng ta khai báo A như một lớp cơ sở ảo (virtual) cho cả B và C. Định nghĩa lớp B và C sẽ thay đổi như sau:
class B: virtual public A
{ public:
double v2;
};
class C: virtual public A
{ public:
float v3;
};
Các lớp cơ sở ảo, có cùng một kiểu lớp sẽ được kết hợp để tạo một lớp cơ sở duy nhất có kiểu đó cho bất kỳ lớp dẫn xuất nào thừa hưởng chúng. Hai lớp cơ sở A ở trên sẽ trở thành một lớp cơ sở A duy nhất cho bất kỳ lớp dẫn xuất nào từ B và C. Và lớp D sẽ chỉ có một cơ sở của lớp A.
Ngay cả với lớp cơ sở ảo vẫn có thể có một lớp với nhiều lớp cơ sở mà lại thừa hưởng nhiều gốc của cùng một lớp.
Ví dụ:
class A
{ public:
int v1;
};
class B: virtual public A
{ public:
double v2;
};
class C: virtual public A
{ public:
float v3;
};
class X: public A
{ public:
double v5;
};
class D: public B, public C, public X
{ public:
char v4;
};
ở đây, lớp D thừa hưởng một lớp cơ sở A từ B và C, và còn thừa hưởng lớp cơ sở A thứ hai từ X. Điều này không rõ ràng vì X không khai báo A là lớp cơ sở ảo.
6.6 HÀM THÀNH PHẦN ẢO VÀ TƯƠNG ỨNG BỘI.
6.6.1 Lời gọi tới hàm thành phần
Lớp dẫn xuất được thừa kế các hàm thành phần của các lớp cơ sở liên quan tới lớp dẫn xuất. Có thể gọi tới các hàm thành phần trong các lớp cơ sở và lớp dẫn xuất.
Ví dụ:
class A
{ public:
void display() { cout <"
Lớp A";}
};
class B: public A
{ public:
void display() { cout <"
Lớp B";}
};
class C: public B
{ public:
void display() { cout <"
Lớp C";}
};
Lớp C có hai lớp cơ sở là lớp A và B. Lớp C kế thừa các hàm của A và B. Do đó một đối tượng của C có 3 hàm thành phần display().
Ví dụ:
C c;
c.display();
c.B::display();
c.A::display();
Các lời gọi hàm thành phần trên đều xuất phát từ đối tượng c và xác định rõ hàm cần gọi.
Xét các lời gọi hàm thành phần không phải từ một biến đối tượng mà từ một con trỏ. Khi đó 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.
Ví dụ:
A *p, *q, *r;
A a;
B b;
C c;
p=&a;
q=&b;
r=&c;
Để gọi tới hàm thành phần, trong lời gọi phải xác định rõ hàm nào (trong các hàm thành phần trùng tên của các lớp kế thừa ) đượ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ì hàm thành phần của lớp đó đượ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ì hàm thành phần của lớp đó sẽ được gọi bất kể con trỏ chứa địa chỉ của đối tượng nào.
Ví dụ: Lời gọi hàm thành phần từ các con trỏ p, q, r
p->display();
q->display();
r->display();
Cả 3 hàm trên đều gọi tới hàm thành phần A::display() vì các con trỏ p,q,r đều có kiểu A.
Ví dụ: Xét 4 lớp A,B,C,D. Lớp B và C có cùng lớp cơ sở A. Lớp D dẫn xuất từ C. Cả 4 lớp có cùng hàm thành phần display(). Xét hàm sau:
void show()
{
p->display();
}
Khi đó, lời gọi hàm luôn luôn gọi tới hàm thành phần A::display() vì con trỏ p kiểu A.
Ví dụ:
#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 display() { cout <<"
Lớp A:"<<n;}
int tell_n() { return n;}
};
class B: public A
{ public:
B(): A() { }
B(int n1): A(n1) {}
void display() { cout <<"
Lớp B:"<<tell_n();}
};
class C: public A
{ public:
C(): A() { }
C(int n1): A(n1) {}
void display() { cout <<"
Lớp C:"<<tell_n();}
};
class D: public C
{
public:
D(): C() { }
D(int n1): C(n1) {}
void display() { cout <<"
Lớp D:"<<tell_n();}
};
void show(A *p)
{ p->display(); }
int main()
{ A a(1); B b(2); C c(3); D d(4);
clrscr();
show(&a); show(&b); show(&c); show(&d);
getch();
return 0;
}
Bốn câu lệnh gọi tới hàm show trong main() đều gọi tới hàm thành phần A::display()
Hạn chế của hàm thành phần theo định nghĩa thông thường.
Việc sử dụng hàm thành phần thông thường trong phát triển chương trình có cấu trúc thừa kế có một số hạn chế.
Ví dụ:
Xây dựng chương trình quản lý thuê bao, mỗi thuê bao gồm ba thuộc tính: Mã số thuê bao, tên thuê bao, và cước phí. Chương trình có 3 chức năng: Nhập dữ liệu thuê bao điện thoại, in dữ liệu ra máy in và kiểm tra, lựa chọn quyết định in hay không. Nội dung chương trình như sau:
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class thuebao
{ private:
char tentb[25];
int mstb;
float cp;
public:
void get_data()
{ cout<< "
Tên thuê bao: "; fflush(stdin);gets(tentb);
cout<< "
Mã số thuê bao: "; cin>>mstb;
cout<< "
Cước phí: "; cin>>cp;
}
void print()
{ fprintf (stdprn, "
Tên thuê bao: %s",tentb);
fprintf(stdprn, "
Mã số thuê bao: %d",mstb);
fprintf(stdprn, "
Cước phí: %0.1f",cp);
}
void check_print()
{ int ch;
cout<<"
Tên thuê bao: "<<tentb;
cout<<"
Có in không ? C/K";
ch=toupper(getch());
if (ch=='C')
this->print();
}
};
void main()
{ thuebao t[100];
int i,n;
cout<<"
Tổng số thuê bao: ";
cin >>n;
for(i=1;i<=n;++i) t[i].get_data();
for(i=1;i<=n;++i) t[i].check_print();
getch();
}
Giả sử chúng ta muốn quản lý thêm địa chỉ của thuê bao. Ta có thể không đả động đến lớp thuebao mà xây dựng lớp mới thuebao2 dẫn xuất từ thuebao. Trong lớp thuebao2 đưa thêm thuộc tính dc (địa chỉ) và các hàm thành phần get_data(), print(). Cụ thể lớp thuebao2 được định nghĩa như sau:
class thuebao2: public thuebao
{
private:
char dc[30];
public:
void get_data()
{ thuebao::get_data();
cout<< "
Địa chỉ thuê báo: ";
fflush(stdin);gets(dc);
}
void print()
{ thuebao::print();
fprintf (stdprn, "
Địa chỉ thuê bao: %s",dc);
}
};
Trong lớp thuebao2 không xây dựng lại hàm thành phần check_print() mà sẽ dùng hàm thành phần check_print() của lớp thuebao. Chương trình mới như sau:
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class thuebao
{
private:
char tentb[25];
int mstb;
float cp;
public:
void get_data()
{ cout<< "
Tên thuê bao: "; fflush(stdin);gets(tentb);
cout<< "
Mã số thuê bao: "; cin>>mstb;
cout<< "
Cước phí: "; cin>>cp;
}
void print()
{ fprintf(stdprn, "
Tên thuê bao: %s",tentb);
fprintf(stdprn, "
Mã số thuê bao: %d",mstb);
fprintf(stdprn, "
Cước phí: %0.1f",cp);
}
void check_print()
{ int ch;
cout<<"
Tên thuê bao: "<<tentb;
cout<<"
Có in không ? C/K";
ch=toupper(getch());
if (ch=='C') this->print();
}
};
class thuebao2: public thuebao
{
private:
char dc[30];
public:
void get_data()
{ thuebao::get_data();
cout<< "
Địa chỉ thuê báo: ";
fflush(stdin);gets(dc);
}
void print()
{ thuebao::print();
fprintf (stdprn, "
Địa chỉ thuê bao: %s",dc);
}
};
int main()
{
thuebao2 t[100];
int i,n;
cout<<"
Tổng số thuê bao: "; cin >>n;
for(i=1;i<=n;++i) t[i].get_data();
for(i=1;i<=n;++i) t[i].check_print();
getch();
return 0;
}
Khi thực hiện chương trình này, ta thấy dữ liệu in ra vẫn không có Địa chỉ thuê bao. Nguyên nhân do câu lệnh t[i].check_print(); trong hàm main() gọi tới hàm thành phần check_print() của lớp thuebao2 (vì t[i] là đối tượng của lớp thuebao2). Nhưng lớp thuebao2 không định nghĩa hàm thành phần thuebao::check_print() sẽ không được gọi tới. Hãy xem hàm thành phần này:
void check_print()
{ int ch
cout<<"
Tên thuê bao: "<<tentb;
cout<<"
Có in không ? C/K";
ch=toupper(getch());
if (ch=='C')
this->print(); // Gọi đến TB::print() vì this là con trỏ kiểu TB)
}
Các lệnh đầu của hàm sẽ in Tên thuê bao. Nếu chọn 'C' thì câu lệnh:
this->print();
sẽ được thực hiện. Mặc dù địa chỉ của t[i] (là đối tượng của lớp TB2) được truyền cho con trỏ this, nhưng câu lệnh này luôn gọi tới hàm thành phần TB::print() vì con trỏ this ở đây có kiểu TB và vì print() là hàm thành phần. Do đó không in được Địa chỉ thuê bao.
Như vậy, việc sử dụng các hàm thành phần print() trong TB và TB2 đã không đáp ứng được yêu cầu phát triển chương trình. Có một giải pháp khắc phục là: Định nghĩa các hàm thành phần print() trong TB và TB2 như là các hàm thành phần ảo (virtual)
6.6.2 Hàm thành phần ảo.
Giả sử A là lớp cơ sở, các lớp B, C, D dẫn xuất từ A và trong 4 lớp trên đều có các hàm thành phần trùng dòng tiêu đề (trùng kiểu, tên và các đối). Khi đó có thể định nghĩa các hàm thành phần này là hàm thành phần ảo như sau:
Hoặc thêm từ khoá virtual vào dòng tiêu đề của hàm thành phần 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, D.
Ví dụ:
Cách thứ nhất.
class A
{ ............
virtual void display() { cout<<"\Đây là lớp A"; }
};
class B: public A
{ ............
void display() { cout<<"\Đây là lớp B"; }
};
class C: public B
{ ............
void display() { cout<<"\Đây là lớp C"; }
};
class D: public A
{ ............
void display() { cout<<"\Đây là lớp D"; }
};
Cách thứ hai.
class A
{ ............
virtual void display() { cout<<"\Đây là lớp A"; }
};
class B: public A
{ ............
virtual void display() { cout<<"\Đây là lớp B"; }
};
class C: public B
{ ............
virtual void display() { cout<<"\Đây là lớp C"; }
};
class D: public A
{ ............
virtual void display() { cout<<"\Đây là lớp D"; }
};
Cần chú ý rằng từ khoá virtual không được đặt bên ngoài định nghĩa lớp.
Ví dụ: Định nghĩa sai:
class A
{ ............
virtual void display();
};
virtual void display()
{ cout<<"\Đây là lớp A"; }
Định nghĩa này cần sửa lại như sau:
class A
{ ............
virtual void display();
};
void display() { cout<<"\Đây là lớp A"; }
Quy tắc gọi hàm thành phần ảo
Hàm thành phần ảo chỉ khác hàm thành phần khi được gọi từ một con trỏ. Lời gọi tới hàm thành phần ảo từ một con trỏ chưa cho biết rõ hàm thành phần nào trong số các hàm thành phần ả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ì hàm thành phần của lớp đó sẽ được gọi.
Ví dụ: Xét 4 lớp A,B,C,D đã định nghĩa ở trên
A *p ; A a; B b; C c; D d
Lời gọi tới các hàm thành phần display() sau:
p=&a; // p trỏ tới đối tượng a của lớp A
p->display(); // Gọi tới A::display()
p=&b; // p trỏ tới đối tượng b của lớp B
p->display(); // Gọi tới B::display()
p=&c; // p trỏ tới đối tượng c của lớp C
p->display(); // Gọi tới C::display()
p=&d; // p trỏ tới đối tượng d của lớp D
p->display(); // Gọi tới D::display()
6.6.3 Tương ứng bội với hàm thành phần ảo.
Chúng ta thấy cùng một câu lệnh
p->display();
tương ứng với nhiều hàm thành phần khác nhau. Điều này được gọi là tương ứng bội. Khả năng này 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 đồ.
Liên kết động.
Có sự khác nhau giữa hàm thành phần và hàm thành phần ảo trong việc liên kết một lời gọi tới một hàm thành phần.
Ví dụ:
A *p; A a; B b; C c; D d
Nếu display() là các hàm thành phần thì dù p chứa địa chỉ của các đối tượng a,b,c,d thì lời gọi hàm:
p->display();
luôn gọi tới hàm thành phần A::display(). Hay là lời gọi tới hàm thành phần luôn liên kết với một hàm thành phần cố định. Cũng với lời gọi hàm thành phần:
p->display();
nhưng nếu display() là các hàm thành phần ảo thì lời gọi này không liên kết cứng với một hàm thành phần cụ thể nào. Hàm thành phần mà nó liên kết còn chưa xác định khi dịch chương trình. Lời gọi này sẽ tương ứng với:
A::display() nếu p chứa địa chỉ đối tượng lớp A
B::display() nếu p chứa địa chỉ đối tượng lớp B
C::display() nếu p chứa địa chỉ đối tượng lớp C
D::display() nếu p chứa địa chỉ đối tượng lớp D
Như vậy, một lời gọi tới hàm thành phần ảo không liên kết với một hàm thành phần cố định, mà tuỳ thuộc vào nội dung con trỏ. Đó là sự liên kết động.
Gán địa chỉ đối tượng cho con trỏ lớp cơ sở
C++ cho phép gán địa chỉ đối tượng của một lớp dẫn xuất cho con trỏ lớp cơ sở.
Ví dụ:
A *p; A a; B b; C c; D d
p=&a; // p trỏ tới đối tượng a của lớp A
p=&b; // p trỏ tới đối tượng b của lớp B
p=&c; // p trỏ tới đối tượng c của lớp C
p=&d; // p trỏ tới đối tượng d của lớp D
Tuy nhiên cần chú ý không được gán địa chỉ đối tượng của lớp cơ sở cho con trỏ của lớp dẫn xuất. Ví dụ như:
B *q; A a; q= &a;
là sai vì gán địa chỉ đối tượng lớp cơ sở A cho con trỏ của lớp dẫn xuất B
Ví dụ:
#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;}
virtual void display() { cout <<"
Lớp A:"<<n;}
int tell_n() { return n;}
};
class B: public A
{
public:
B(): A() { }
B(int n1): A(n1) {}
void display() { cout <<"
Lớp B:"<<tell_n();}
};
class C: public A
{ public:
C(): A(){ }
C(int n1): A(n1){}
void display() { cout <<"
Lớp C:"<<tell_n();}
};
class D: public C
{ public:
D(): C(){ }
D(int n1): C(n1){}
void display() { cout <<"
Lớp D:"<<tell_n();}
};
void show(A *p)
{ p->display(); }
int main()
{ A a(1); B b(2); C c(3); D d(4);
clrscr();
show(&a); show(&b); show(&c); show(&d);
getch();
return 0;
}
Trong ví dụ trên, chúng ta định nghĩa display() là hàm thành phần ảo. Khi đó 4 câu lệnh:
show(&a); show(&b); show(&c); show(&d);
trong hàm main() sẽ lần lượt gọi tới 4 hàm thành phần khác nhau:
A::display(), B:: display(), C::display(), D::display()
6.6.4 Kế thừa của các hàm thành phần ảo.
Cũng giống như các hàm thành phần thông thường khác, hàm thành phần ảo cũng có tính thừa kế. Ví dụ như trong chương trình trên, nếu ta bỏ đi hàm thành phần display() của lớp D, thì câu lệnh :
display(&d);
sẽ gọi tới hàm thành phần C::display(),hàm thành phần này được kế thừa trong lớp D (vì lớp D dẫn xuất từ C)
6.6.5 Sử dụng hàm thành phần ảo trong phát triển chương trình
Chúng ta có thể sử dụng hàm thành phần ảo để khắc phục hạn chế của hàm thành phần trong việc sử dụng tính thừa kế khi phát triển chương trình.
Ví dụ:
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
class thuebao
{ private:
char tentb[25];
int mstb;
float cp;
public:
void get_data()
{ cout<< "
Tên thuê bao: "; fflush(stdin);gets(tentb);
cout<< "
Mã số thuê bao: "; cin>>mstb;
cout<< "
Cước phí: "; cin>>cp;
}
virtual void print()
{ fprintf (stdprn, "
Tên thuê bao: %s",tentb);
fprintf(stdprn, "
Mã số thuê bao: %d",mstb);
fprintf(stdprn, "
Cước phí: %0.1f",cp);
}
void check_print()
{ int ch;
cout<<"
Tên thuê bao: "<<tentb;
cout<<"
Có in không ? C/K";
ch=toupper(getch());
if (ch=='C') this->print();
}
};
class thuebao2: public thuebao
{
private:
char dc[30];
public:
void get_data()
{ thuebao::get_data();
cout<< "
Địa chỉ thuê báo: ";
fflush(stdin);gets(dc);
}
void print()
{ thuebao::print();
fprintf (stdprn, "
Địa chỉ thuê bao: %s",dc);
}
};
int main()
{ thuebao2 t[100];
int i,n;
cout<<"
Tổng số thuê bao: "; cin >>n;
for(i=1;i<=n;++i) t[i].get_data();
for(i=1;i<=n;++i) t[i].check_print();
getch();
return 0;
}
Trong ví dụ trên, chúng ta đã định nghĩa hàm thành phần print() là hàm thành phần ảo.
Khi thực hiện chương trình này, ta thấy dữ liệu in ra đã có Địa chỉ thuê bao. Nguyên nhân do câu lệnh:
t[i].check_print();
trong hàm main() gọi tới hàm thành phần check_print() của lớp thuebao2 (vì t[i] là đối tượng của lớp thuebao2). Nhưng lớp thuebao2 không định nghĩa hàm thành phần check_print(), nên thuebao::check_print() sẽ được gọi tới. Hãy xem hàm thành phần này:
void check_print()
{ int ch
cout<<"
Tên thuê bao: "<<tentb;
cout<<"
Có in không ? C/K";
ch=toupper(getch());
if (ch=='C')
this->print(); // Gọi đến thuebao::print() vì this là con trỏ kiểu thuebao)
}
Các lệnh đầu của hàm sẽ in Tên thuê bao. Nếu chọn 'C' thì câu lệnh:
this->print();
sẽ được thực hiện. Địa chỉ của t[i] (là đối tượng của lớp thuebao2) được truyền cho con trỏ this (của lớp cơ sở thuebao). Vì print() là hàm thành phần ảo và vì this đang trỏ tới đối tượng t[i] của thuebao2 nên câu lệnh này gọi tới hàm thành phần thuebao2::print() . Trong hàm thành phần thuebao2::print() có in Địa chỉ của thuê bao.
Như vậy, việc sử dụng các hàm thành phần print() trong thuebao và thuebao2 đã không đáp ứng được yêu cầu phát triển chương trình. Có một giải pháp khắc phục là: Định nghĩa các hàm thành phần print() trong thuebao và thuebao2 như là các hàm thành phần ảo
6.7 CÁC LỚP CƠ SỞ TRỪU TƯỢNG (ABTRACTION BASE CLASSES)
Một lớp cơ sở là một lớp chỉ được dùng làm cơ sở cho các lớp khác. Không hề có đối tượng nào của một lớp trừu tượng được tạo ra cả, bởi vì nó chỉ được dùng để định nghĩa một số khái niệm tổng quát, chung cho các lớp khác. Ví dụ về lớp trừu tượng là lớp CUOC_PHI, nó sẽ được dùng làm cơ sở để xây dựng các lớp cụ thể như lớp CUOC_NOI_TINH, CUOC_LIEN_TINH....
Trong C++, thuật ngữ "lớp trừu tượng" được áp dụng cho các lớp có chứa các hàm thành phần ảo thuần tuý. Hàm thành phần ảo thuần uý là một hàm thành phần ảo mà nội dung của nó không có gì. Cách thức định nghĩa một hàm thành phần ảo thuần tuý như sau:
virtual void tên_hàm() = 0;
Ví dụ:
Class A
{ public:
virtual void get_data()=0;
virtual void tell_data()=0;
void process();
};
Trong ví dụ, A là lớp cơ sở trừu tượng. Các hàm thành phần get_data() và tell_data() được khai báo là các hàm thành phần ảo thuần tuý (bằng cách gán số 0 cho chúng thay cho việc cài đặt các hàm này). Hàm thành phần process() là một hàm bình thường và được định nghĩa tại một chỗ nào đó.
Không có đối tượng nào của một lớp trừu tượng lại có thể được phát sinh. Tuy nhiên các con trỏ và các biến tham chiếu đến các đối tượng của lớp trừu tượng thì vẫn hợp lệ. Bất kỳ lớp nào dẫn xuất từ một lớp cơ sở trừu tượng phải định nghĩa lại tất cả các hàm thành phần ảo thuần tuý mà nó thừa hưởng, hoặc bằng các hàm thành phần ảo thuần tuý, hoặc bằng các định nghĩa thực sự.
Ví dụ:
Class D: public A
{ public:
virtual void get_data()=0;
virtual void tell_data()
{ ............ }
};
Theo ý nghĩa về hướng đối tượng, ta vẫn có thể có một lớp trừu tượng mà không nhất thiết phải chứa những hàm thành phần ảo thuần tuý.
Nói chung, bất kỳ lớp nào mà nó chỉ được dùng làm cơ sở cho những lớp khác đều có thể được gọi là lớp trừu tượng. Một cách dễ dàng để nhận biết một lớp trừu tượng là xem có dùng lớp đó để khai báo các đối tượng hay không. Nếu không thì đó là lớp cơ sở trừu tượng
Ví dụ:
Xây dựng chương trình quản lý danh sách học viên với các chức năng sau:
+ Nhập một học viên mới (cao đẳng hay đại học) vào phần tử đầu tiên.
+ Xoá một học viên (cao đẳng hay đại học)
+ Thống kê các học viên trong danh sách
Chương trình được tổ chức như sau:
+ Định nghĩa lớp hoc_vien là lớp cơ sở ảo. Lớp này có một thuộc tính là tên học viên và một hàm thành phần ảo dùng để xưng tên.
+ Hai lớp hv_cao_dang và hv_dai_hoc được dẫn xuất từ lớp hoc_vien.
+ Cuối cùng là lớp ds_hoc_vien dùng để quản lý chung cả học viên đại học và học viên cao đẳng. Lớp này có 3 thuộc tính là số học viên tối đa, số học viên hiện tại và một mảng con trỏ kiểu hoc_vien. Mỗi phần tử mảng chứa địa chỉ của một đối tượng kiểu hv_cao_dang hoặc hv_dai_hoc.
Nội dung chương trình như sau:
#include <conio.h>
#include <stdio.h>
#include <iostream.h>
#include <ctype.h>
#include <string.h>
class hoc_vien
{ protected:
char *ten;
public:
hoc_vien() { ten=NULL; }
hoc_vien(char *ten1) { ten=strdup(ten1); }
virtual void xung_ten() {}
};
class hv_cao_dang: public hoc_vien
{ public:
hv_cao_dang():hoc_vien() { }
hv_cao_dang(char *ten1):hoc_vien(ten1) { }
virtual void xung_ten() { cout <<"
HV cao đẳng: "<<ten; }
};
class hv_dai_hoc: public hoc_vien
{ public:
hv_dai_hoc():hoc_vien() {}
hv_dai_hoc(char *ten1):hoc_vien(ten1) { }
virtual void xung_ten() { cout<<"
HV đại học: "<<ten; }
};
class ds_hoc_vien
{ private:
int max_so_hv;
int so_hv;
hoc_vien **h;
public:
ds_hoc_vien(int max);
~ds_hoc_vien();
int nhap(hoc_vien *c);
hoc_vien* xuat(int n);
void thong_ke();
};
ds_hoc_vien::ds_hoc_vien(int max)
{ max_so_hv=max; so_hv=0;
h=new hoc_vien*[max];
for(int i=0;i<max;++i) h[i]=NULL;
}
ds_hoc_vien::~ds_hoc_vien()
{ max_so_hv=0; so_hv=0;
delete h;
}
int ds_hoc_vien::nhap(hoc_vien *c)
{ if (so_hv==max_so_hv) return 0;
int i=0;
while (h[i]!=NULL) ++i;
h[i]=c;
so_hv++;
return (i+1);
}
hoc_vien* ds_hoc_vien::xuat(int n)
{ if (n<1||n>max_so_hv) return NULL;
--n;
if (h[n])
{ hoc_vien *c=h[n];
h[n]=NULL;
so_hv--;
return c;
}
else
return NULL;
}
void ds_hoc_vien::thong_ke()
{ if (so_hv)
{ cout<<"
";
for (int i=0;i<max_so_hv;++i)
if (h[i]) h[i]->xung_ten();
}
};
hv_dai_hoc c1("TUAN"); hv_dai_hoc c2("LE");
hv_dai_hoc c3("HA"); hv_dai_hoc c4("DUNG"); hv_dai_hoc c5("LAN");
hv_cao_dang m1("NHU"); hv_cao_dang m2("HIEN");
hv_cao_dang m3("LAN"); hv_cao_dang m4("VAN"); hv_cao_dang m5("PHUONG");
int main()
{ ds_hoc_vien d(20);
clrscr();
d.nhap(&c1);
int im2=d.nhap(&m2);
d.nhap(&c3); d.nhap(&m1);
int ic4=d.nhap(&c4);
d.nhap(&c5); d.nhap(&m5); d.nhap(&c2); d.nhap(&m3);
d.thong_ke();
d.xuat(im2); d.xuat(ic4);
d.thong_ke();
getch();
return 0;
}
Theo quan điểm chung về cách thức sử dụng, lớp hoc_vien là lớp cơ sở trừu tượng. Nhưng theo quan điểm của C++ thì lớp này chưa phải là lớp cơ sở trừu tượng, vì trong lớp không có các hàm ảo thuần tuý. Hàm xung_ten
virtual void xung_ten() { }
là hàm ảo và được định nghĩa đầy đủ, mặc dù thân của nó là rỗng. Do đó khai báo:
hoc_vien hv("Học viên");
vẫn được C++ chấp nhận.
Bây giờ nếu định nghĩa lại hàm thành phần xung_ten() như sau:
virtual void xung_ten()=0;
thì nó trở thành hàm thành phần ảo thuần tuý và C++ sẽ quan niệm lớp hoc_vien là lớp trừu tượng. Khi đó khai báo:
hoc_vien hv("Học viên");
sẽ bị lỗi với thông báo:
Cannot create instance of abstruct class 'hoc_vien'
Bạn đang đọc truyện trên: AzTruyen.Top