- Nếu trong lớp cơ sở không có constructor có tham số thì trong lớp dẫn xuất không cần phải có constructor.
- Nếu trong lớp cơ sở có constructor có tham số thì trong lớp dẫn xuất phải có constructor có tham số gồm 2 thành phần: thành phần tơng ứng của lớp cơ sở và thành phần tham số của nó. Khi đó:
+ Constructor của lớp cơ sở thực hiện trớc rồi mới đến constructor của lớp dẫn xuất thực hiện.
+ Trong trờng hợp kế thừa bội thì constructor của các lớp cơ sở đợc thực hiện theo thứ tự của các lớp cơ sở đ- ợc khai báo mode kế thừa ở lớp dẫn xuất.
+ Trong trờng hợp kế thừa đa mức thì các constructor của lớp cơ sở đợc thực hiện theo thứ tự kế thừa ở các mức.
- Định nghĩa constructor ở lớp dẫn xuất nh sau: contructor_dẫn_xuất(dx1,dx2,...,dxn,dxx): contructor _tử_cơ_sở1(dx1), contructor _tử_cơ_sở2(dx2), ... contructor _cơ_sởn(dxn), {
// nội dung constructor của lớp dẫn xuất }
Ví dụ 5: Trong kế thừa đa mức
#include <iostream.h> #include <conio.h> #include <stdio.h> class A { int a; public: A() {a=0;}
A(int a1) { a=a1;}
void put_A() { cout<<" a = "<<a<<endl;} };
class B:public A { int b;
public: B() {b=0;}
B(int a1,int b1): A(a1) { b=b1;}
void put_B() { cout<<" b = "<<b<<endl;} };
class C:public B { int c;
public: C() {c=0;}
C(int b1,int b2,int c1): B(b1,b2) { c=c1;} void put_C() { cout<<" c = "<<c<<endl;} }; int main() { clrscr(); B n(100,200); n.put_A(); n.put_B(); cout<<endl; C m(10,20,30); m.put_A(); m.put_B(); m.put_C(); getche(); return 0; }
Ví dụ 6: Trong kế thừa bội
#include <iostream.h> #include <conio.h> #include <stdio.h> class A { int a; public: A() {a=0;}
A(int a1) { a=a1;}
void put_A() { cout<<" a = "<<a<<endl;} }; class B { int b; public: B() {b=0;} B(int b1) { b=b1;}
void put_B() { cout<<" b = "<<b<<endl;} };
class C:public A,public B { int c;
C() {c=0;}
C(int a1,int b1,int c1): A(a1),B(b1) { c=c1;} void put_C() { cout<<" c = "<<c<<endl;} }; int main() { clrscr(); C m(10,20,30); m.put_A(); m.put_B(); m.put_C(); getche(); return 0; }
Bài 2. Con trỏ xác định thành phần của lớp và con trỏ xác định đối tợng 1. Các ký hiệu cú pháp về con trỏ Xét lớp class M { public: int x; void show(void); } ;
Khi đó việc khai báo con trỏ đối với lớp M có các cú pháp sau đây:
Cú pháp ý nghĩa, ví dụ
Để khai báo 1 con trỏ, trỏ đến 1 thành phần của lớp M
Ví dụ M::*ptr ;
Khi đó ptr có thể trỏ đến dữ liệu thành phần x hoặc hàm thành phần show(). Thờng thì ptr dùng để chứa địa chỉ của các thành phần x và show. Địa chỉ của các thành phần này nh chỉ ra dới đây. Để khai báo địa chỉ của 1 thành phần nào đó của lớp M.
Ví dụ
&M::x là địa chỉ của thành phần dữ liệu x
&M::show là địa của thành phần hàm show()
Khi đó, có thể viết
M:: *
M::*px=&M::x;
void (M::*pf)()=&M::show;
Để khai báo 1 con trỏ, trỏ đến một đối t- ợng trong lớp M.
Ví dụ
M*ptr; Khai báo con trỏ ptr để trỏ đến 1 đối tợng nào đó trong lớp M.
Khi đó, có thể viết M*pm = &m;
Rất hay sử dụng trong kế thừa để cài đặt khái niệm tơng ứng bội bằng cơ chế hàm ảo. Nếu: M m; int M::*px = &M::x; void (M::*pf)() = &M::show; M*ptr = &m; Thì khi đó ta có các lệnh tơng đơng m.x m.show() m m.*px; pm->*px; pm->x; (m.*pf)(); (pm->*pf)(); pm->show(); pm->m 2. Các ví dụ Ví dụ 1: #include <iostream.h> #include <conio.h> class M { public: int x;
void show() { cout <<"x= "<<x<<endl; } }; 125 M * Kết quả: 10 10 10 x=10 x=10 x=10
int main() { M m; int M::* px= &M::x; void (M::* pf)()= &M::show; M* pm = &m; clrscr(); m.x=10; cout<<m.*px<<endl; cout<<pm->*px<<endl; cout<<pm->x<<endl<<endl; (m.*pf)(); (pm->*pf)(); pm->show(); cout<<endl; pm->*px=20; (pm->*pf)(); getche(); return 0; } Nhận xét
- Toán tử ->* dùng để truy nhập tới thành phần của đối tợng khi đối tợng của lớp và thành phần của đối tợng đều xác định bởi con trỏ.
Ví dụ pm->*px (pm->*pf)()
- Toán tử.* dợc sử dụng khi đối tợng đợc chỉ ra trực tiếp còn các thành phần của nó thì đợc xác định nhờ con trỏ. Ví dụ m.*px; (m.*pf)() Ví dụ 2: #include <iostream.h> #include <conio.h> #include <stdio.h> class M { int x,y; public: Kết quả Tong = 30 Tong = 70
void set_xy(int a,int b) {x=a; y=b;} friend int sum(M);
}; int sum(M m) { int M::* px=&M:: x; int M::* py=&M:: y; M* pm=&m; int s=pm->*px + pm->*py; return s; } int main() { clrscr(); M m; void (M::*pf)(int,int)=&M::set_xy; (m.*pf)(10,20);
cout << "Tong = "<<sum(m)<<endl; M *ptr=&m;
(ptr->*pf)(30,40);
cout << "Tong = "<<sum(*ptr)<<endl; getche();
return 0; }
3. Con trỏ xác định đối tợng
Trong C++, việc sử dụng con trỏ để trỏ vào đối tợng của lớp rất hay sử dụng vì các lý do sau đây
- Cho phép cấp phát bộ nhớ động cho 1 mảng đối t- ợng.
- Cho phép sử dụng con trỏ this để trỏ vào đối tợng. - Cho phép sử dụng con trỏ có kiểu của lớp trong quan hệ kế thừa (xem bài 3).
3.1. Cho phép cấp phát bộ nhớ động cho 1 mảng đối t- ợng
#include <iostream.h> #include <conio.h> #include <stdio.h> class MAT_HANG { int code; float price; public:
void getdata(int c,float p) {code=c; price=p;}
void show(void) { cout<<"ma hang: "<<code<<endl; cout<<"don gia: "<<price<<endl; } };
const int k=2; int main() { clrscr();
MAT_HANG *p = new MAT_HANG[k]; MAT_HANG *d = p;
int i,x; float y;
for(i=0;i<k;i++)
{ cout<<"nhap ma va don gia mat hang thu " <<i+1<<": "; cin>>x>>y; p->getdata(x,y); p++; } for(i=0;i<k;i++) {d->show(); d++; } getche(); return 0; } 3.2. Con trỏ this
* C++ sử dụng con trỏ this là con trỏ đặc biệt để biểu diễn cho đối tợng có liên quan đến hàm thành phần đang thực hiện.
Ví dụ xét constructor (hoặc 1 hàm thành phần bất kỳ) của lớp A class A { int x; A(int x1) { x=x1;} ... };
thực chất lệnh trên sử dụng con trỏ this xác định cho 1 đối tợng ẩn để trỏ vào thành phần dữ liệu x:
A(int x1) {this->x=x1;}
* Trong toán tử tải bội (không thân thiện) thì nếu số ngôi của toán tử gốc là 2 thì số ngôi của toán tử tải bội sẽ là 1.
Ví dụ class A { int x,y; public:
A operator +(A a) { A b ; b.x=x+a.x; b.y=y+a.y; return b;}
};
Thực chất, toán tử tải bội + nói trênvẫn đủ 2 ngôi. Ngôi thứ nhất là đối tợng ẩn xác định bằng con trỏ this, ngôi thứ 2 là đối a của toán tử.
A operator +(A a) { A b; b.x=this->x+a.x; b.y=this->y+a.y; return b;} Ví dụ 4 #include <iostream.h> #include <conio.h> #include <stdio.h> class A { int x,y; public: A() {x=y=0;}
A(int x1,int y1) {this->x=x1; this->y=y1;} A operator +(A a)
{A b; b.x=this->x+a.x; b.y=this->y+a.y; return b;} void show() {cout<<this->x<<","<<this->y<<endl;} }; int main() { clrscr(); A a(3,4),b(5,6); a=a+b; a.show(); getche(); return 0; }
Một ứng dụng quan trọng của con trỏ this là nó trả lại đối tợng mà nó trỏ tới bằng lệnh return *this trong tr- ờng hợp mà khai báo tên đối tợng là không thuận lợi.
Ví dụ 5 #include <iostream.h> #include <conio.h> #include <string.h> class CANBO { char *ten; int tuoi; public: CANBO(char*,int); CANBO& gia_hon(CANBO&); void xem(void); }; CANBO::CANBO(char *s,int a) { int k=strlen(s);
ten = new char[k+1]; strcpy(ten,s); tuoi=a;} CANBO& CANBO::gia_hon(CANBO &x) { if (x.tuoi>=tuoi) return x;
else return *this; }
{ cout<<"ten:"<<ten<<endl; cout<<"tuoi:"<<tuoi<<endl; }
int main() { clrscr();
CANBO a("TUNG QUAN",32), b("CHI DUNG",25),
c("THANH THAO",38); CANBO p("",0);
p=a.gia_hon(b);
cout<<"nguoi lon tuoi hon la: "; p.xem();
p=p.gia_hon(c);
cout<<"nguoi lon tuoi nhat la: "; p.xem();
getche(); return 0; }
Bài 3. Hàm ảo và tơng ứng bội trong kế thừa
1. Các hàm dịch chuyển (Override function)
- C++ cho phép lớp cơ sở và lớp dẫn xuất có thể cùng khai báo 2 hàm cùng tên thậm chí cùng đặc tính (cùng đặc tính đợc hiểu là cùng số lợng đối số, kiểu của đối số và cùng kiểu). Hai hàm nh vậy không phải là 2 hàm tải bội vì chúng không phải là hàm thành phần của cùng một lớp, nhng chúng lại thể hiện tính tơng ứng bội trong kế thừa.
- Một hàm đợc khai báo trong lớp dẫn xuất gọi là hàm dịch chuyển (Override function) từ một hàm trong lớp cơ sở nếu 2 hàm này cùng đặc tính. Sự định nghĩa lại các hàm nh vậy ở lớp dẫn xuất đợc gọi là gọi là sự "dịch chuyển" (Overriding). Ví dụ Class A { int x; public: A(int x1) {x=x1; }
void show() { cout << "show x = "<<x << endl; } void display() { cout << "display x = "<<x << endl; }
};
Class B: public A { int y;
public:
B(int x1,int y1):A(x1) {y=y1; }
void show() { cout << "y = "<<y << endl; } };
- Vấn đề đặt ra là khi đối tợng của lớp dẫn xuất sử dụng hàm dịch chuyển thì hàm đợc triệu gọi là thuộc lớp cơ sở hay lớp dẫn xuất ?
C++ quy định nh sau (Quy tắc 1)
+ Khi đối tợng của lớp dẫn xuất truy nhập tới hàm dịch chuyển thì mặc định là hàm của chính lớp dẫn xuất đó.
Ví dụ B b(10,20);
b.show() // gọi hàm của lớp B A::show() // gọi hàm của lớp A
+ Khi đối tợng của lớp cháu truy nhập tới hàm dịch chuyển của các lớp cơ sở phía trên (bản thân hàm này không có mặt trong lớp cháu) thì mặc định là hàm của lớp cơ sở khai báo sau cùng (gần lớp cháu nhất)
2. Con trỏ xác định đối tợng trong quan hệ kế thừa thừa
- Con trỏ có kiểu dữ liệu là lớp cơ sở thì đơng nhiên có thể trỏ đến đối tợng của lớp cơ sở (nếu ta gán con trỏ bằng địa chỉ của đối tợng thuộc lớp cơ sở) nhng hơn nữa nó còn có khả năng trỏ và đối tợng của lớp dẫn xuất (nếu tiếp theo ta gán con trỏ này bằng địa chỉ của một đối tợng thuộc lớp dẫn xuất) Ví dụ class A { // nội dung lớp B } ; class B: public A { // nội dung lớp B } ; A a; B b; A *ptr; // ptr có kiểu lớp A
ptr=&a; // ptr trỏ đến đối tợng a của lớp A ...
ptr=&b;
// tiếp theo ptr trỏ đến đối tợng a của lớp A ...
- Tuy nhiên, trong trờng hợp này thì khi sử dụng con trỏ để xác định đến hàm dịch chuyển thì chơng trình dịch mặc định là hàm thành phần của lớp cơ sở, mặc dù con trỏ đã đợc gán địa chỉ của đối tợng thuộc lớp dẫn xuất. (ta gọi là Quy tắc 2)
Điều này chứng tỏ việc xác định hàm thành phần thuộc lớp nào là phụ thuộc vào kiểu con trỏ chứ không phụ thuộc vào đối tợng mà con trỏ đang trỏ tới.
Ví dụ1 (cungten1.cpp) #include <iostream.h> #include <conio.h> #include <stdio.h> class A { int x; public: A(int x1) {x=x1;}
void show() {cout<<"Lop A: "<<x<<endl;} };
class B:public A { int y;
public:
B(int x1,int y1):A(x1) {y=y1;}
void show() {cout<<"Lop B: "<<y<<endl;} }; int main() { clrscr(); A a(3); B b(5,6); A *ptr; Kết quả Lop A: 3 Lop A: 5
ptr=&a;
ptr->show(); // goi ham show() cua lop A ptr=&b;
ptr->show();// van goi ham show() cua lop A getche();
return 0; }
3. Con trỏ hàm ảo và tơng ứng bội 3.1. Khái niệm về liên kết tĩnh 3.1. Khái niệm về liên kết tĩnh
- Ta đã biết rằng (quy tắc 2) một con trỏ thuộc lớp cơ sở có thể nhận địa chỉ của bất kỳ đối tợng thuộc các lớp con cháu đợc dẫn xuất từ lớp cơ sở. Tuy nhiên lúc con trỏ này luôn luôn liên kết với các hàm thành phần thuộc lớp cơ sở trong trờng hợp đây là các hàm dịch chuyển. Mối liên kế có tính chất không thay đổi nh vậy gọi là liên kết tĩnh.
3.2. Khái niệm về liên kết động
- C++ đa ra nguyên lý liên kết động cho phép một con trỏ kiểu lớp cơ sở sau khi gán bằng địa chỉ của đối tợng thuộc lớp dẫn xuất nào thì tơng ứng nó sẽ thể liên kết với hàm của chính lớp dẫn xuất đó, mặc dù hàm này là dịch chuyển từ hàm của lớp cơ sở. Cấu trúc cho phép một con trỏ có kiểu cố định nhng có khả năng liên kết với các hàm thành phần dịch chuyển thuộc các lớp khác nhau gọi là liên kết động.
- Cơ chế liên kết tĩnh và liên kết động trong quan hệ kế thừa để phân định việc truy nhập tới các hàm dịch chuyển trong các lớp có quan hệ kế thừa là một trờng hợp cài đặt cụ thể của khái niệm tơng ứng bội trong lập trình h- ớng đối tợng với C++.
- Liên kết động đợc cài đặt bằng cơ chế hàm ảo và nó đợc báo hiệu là hàm ảo bằng cách đặt từ khoá virtual phía trớc khai báo hàm.
Ví dụ 2
#include <iostream.h> #include <conio.h> class A
{ public:
virtual void display(void) {cout<<"Lop A: display()"<<endl;}
void show(void) {cout<<"Lop A: show()"<<endl;} };
class B:public A { public:
void display(void)
{cout<<"Lop B: display()"<<endl;}
void show(void) {cout<<"Lop B: show()"<<endl;} }; int main(){ A a; B b; A *ptr; ptr = &a; ptr->display(); ptr->show(); cout<<endl; ptr= &b; ptr->display(); ptr->show(); cout<<endl; getch(); return 0;}
- Ví dụ trên cho thấy sau khi con trỏ ptr kiểu lớp A, đợc gán bằng địa chỉ của đối tợng b thuộc lớp dẫn xuất B của lớp A thì ptr có thể liên kết với hàm display() của chính lớp B vì hàm display() của lớp A đợc khai báo ảo. Ngợc lại con trỏ ptr vẫn liên kết với hàm show() của lớp cơ
Kết quả
Lop A: display() Lop A: show() Lop B: display() Lop A: show()
sở A chứ không phải của lớp dẫn xuất B vì hàm show() trong lớp cơ sở A không đợc khai báo ảo.
3.3. Các hàm ảo dịch chuyển (Virtual override function)
Khi có một hàm đợc khai báo ảo thì trong lớp dẫn xuất của nó nếu định nghĩa lại hàm này cùng tham số và kiểu tham số thì phải cùng kiểu. Nếu trong lớp dẫn xuất khai báo hàm cùng đặc tính với hàm ảo của lớp cơ sở thì tự khắc nó trở thành hàm ảo mà không phụ thuộc vào hàm đó đợc hoặc không đợc khai báo từ khóa virtual phía trớc, Và, hàm này đợc gọi là hàm ảo dịch chuyển (virtual override
function) từ hàm ảo tơng ứng của lớp cơ sở.
- Trong các ví dụ trên ta sử dụng một biến con trỏ để xác định một đối tợng trong lớp. Trong ví dụ sau đây ta sẽ sử dụng trực tiếp con trỏ đối tợng.
Ví dụ 3 #include <iostream.h> #include <conio.h> #include <stdio.h> class A{ public:
virtual void display() { cout << "\n Class A";} };
class B{ public:
virtual void display() {{ cout<<"\n Class B";} };
void show(A *a) { a->display(); } void main() {
A* a = new A; B* b = new B; a->display(); b->display();
show(b); // goi ham B::display() getch();
}
- Trong ví dụ này, nếu hàm display không đợc khai báo ảo (ở lớp A) thì show(a) sẽ gọi hàm A::display() và
show(b) cũng gọi hàm A::display() vì tham số truyền B* sẽ đợc chuyển kiểu thành A* trớc khi gọi hàm.
4. Kế thừa lai ghép và lớp cơ sở ảo
- Lớp CON kế thừa trực tiếp từ lớp CHA và lớp ME, ngoài ra lớp CON còn kế thừa từ lớp ONGBA theo 2 con đ- ờng khác nhau. Nó kế thừa trực tiếp một số đặc tính từ lớp ONGBA (đờng chấm chấm) và kế thừa gián tiếp một số đặc tính qua 2 lớp CHA và lớp ME.
- Nh vậy thành phần protected và public của lớp ONGBA sẽ đợc kế thừa đúp ở lớp CON, lần thứ nhất kế thừa từ lớp CHA, lần thứ 2 kế thừa từ lớp ME, nghĩa là lớp CON sẽ có 2 tập thành phần kế thừa khác nhau. Vậy vấn đề đặt ra là phải có cơ chế loại bỏ d thừa trong quan hệ kế thừa lai ghép.
- C++ cho phép loại bỏ d thừa trong quan hệ kế thừa lai ghép bằng cơ chế lớp cơ sở ảo và nó đợc chỉ ra bằng từ khoá virtual đặt sau tên lớp và trớc mode private / public. Khi 1 lớp đợc khai báo là lớp cơ sở ảo thì chỉ có 1 bản sao duy nhất những thành phần đợc kế thừa đợc truyền cho lớp kế thừa cho dù có bao nhiêu đờng kế thừa cũng thế.
Ôngbà
Mẹ Cha
class A // ONGBA { // noi dung lop A };
class B1: virtual public A // CHA { // noi dung lop B1};
class B2: virtual public A // ME { // noi dung lop B2 };
class C: public B1, public B2 // CON { // noi dung lop C};
Ví dụ 4 : #include <iostream.h> #include <conio.h> class A { protected: int x; public:
virtual void show() { cout <<" LOP A "<<endl; }