- Lớp dẫn xuất sẽ thừa kế các thành phần dữ liệu, hàm của lớp cơ sở, đồng thời thêm vào các thành phần mới, bao hàm cả việc làm “tốt hơn” hoặc làm lại những công việc mà trong lớp cơ sở
Trang 1Chương 4:
SỰ KẾ THỪA
Trang 2Nội dung chương 4
1.Giới thiệu về sự kế thừa
1.1.Khái niệm
1.2.Ví dụ
1.3.Ý nghĩa
1.4.Chương trình minh họa
2 Kế thừa đơn
2.1.Khái niệm
2.2.Ví dụ
2.3.Phương thức thiết lập và hủy bỏ
2.4.Sự tự động kế thừa các đặc tính lớp cha
2.5.Định nghĩa lại thao tác ở lớp con
2.6.Ràng buộc ngữ nghĩa ở lớp con
Trang 3Nội dung chương 4 (tt)
2.7.Phạm vi truy xuất
2.7.1.Truy xuất theo chiều dọc 2.7.2.Truy xuất theo chiều ngang 2.8.Con trỏ và sự kế thừa
3.Đa kế thừa
Trang 41.Giới thiệu về sự kế thừa
Trang 51.1.Khái niệm về sự kế thừa
- Sự kế thừa là một đặc điểm của ngôn ngữ dùng để
biểu diễn mối quan hệ đặc biệt giữa các lớp Các lớp được trừu tượng hóa và tổ chức thành một sơ đồ phân cấp lớp
- Sự kế thừa là một mức cao hơn của trừu tượng hóa, cung cấp một cơ chế gom chung các lớp có liên quan với nhau thành một mức khái quát hóa đặc trưng cho toàn bộ các lớp nói trên
- Các lớp với các đặc điểm tương tự nhau có thể được tổ chức thành một sơ đồ phân cấp kế thừa Lớp ở trên cùng là trừu tượng hóa của toàn bộ các lớp ở bên dưới nó
Trang 61.1.Khái niệm về sự kế thừa
- Thừa kế cho phép ta định nghĩa 1 lớp mới , gọi là lớp con (subclass) hay lớp dẫn xuất (derived class) từ một lớp đã có, gọi là lớp cha (superclass) hay lớp cơ sở (base class).
- Lớp dẫn xuất sẽ thừa kế các thành phần (dữ liệu, hàm) của lớp cơ sở, đồng thời thêm vào các thành phần mới, bao hàm cả việc làm “tốt hơn” hoặc làm lại những công việc mà trong lớp cơ sở chưa làm tốt hoặc không còn phù hợp với lớp dẫn xuất
Trang 71.1.Khái niệm về sự kế thừa
- Thừa kế cho phép nhiều lớp có thể dẫn xuất từ 1 lớp cơ sở
- Thừa kế cũng cho phép một lớp có thể là dẫn xuất của nhiều lớp cơ sở
- Thừa kế không chỉ giới hạn ở 1 mức: Một lớp dẫn xuất có thể là lớp cơ sở cho các lớp dẫn xuất khác
Trang 81.2.Ví dụ về sự kế thừa
• Ta tổ chức lớp sinh viên kế thừa từ lớp người Lớp người được gọi là lớp cha (superclass) hay lớp cơ sở (base class) Lớp sinh viên được gọi là lớp con
(subclass) hay lớp dẫn xuất (derived class)
• Ngoài ra, ta có thể tổ chức 2 lớp nam sinh và lớp nữ sinh là 2 lớp con (lớp dẫn xuất) của lớp sinh viên Trường hợp này, lớp sinh viên trở thành lớp cha (lớp
cơ sở) của 2 lớp này
Trang 91.2.Ví dụ về sự kế thừa
Hình 4.1: Sơ đồ phân cấp kế thừa
Trang 101.2.Ví dụ về sự kế thừa
Trang 111.2.Ví dụ về sự kế thừa
Hình 4.2: Sơ đồ phân cấp kế thừa
Trang 121.3.Ý nghĩa sự kế thừa
• Kế thừa tạo khả năng xây dựng lớp mới từ lớp đã có, trong đó hàm thành phần được thừa hưởng từ
lớp cha Trong C++, kế thừa còn định nghĩa sự
tương thích, nhờ đó ta có cơ chế chuyển kiểu tự
động
• Kế thừa vừa có khả năng tạo cơ chế khái quát hoá vừa có khả năng chuyên biệt hoá.
• Kế thừa cho phép tổ chức các lớp chia sẻ mã
chương trình chung nhờ vậy có thể dễ dàng sửa
chữa, nâng cấp hệ thống
Trang 131.4.Chương trình minh họa
~Nguoi() {delete [] HoTen;}
void An() const { cout<<HoTen<<" an 3 chen com";} void Ngu() const { cout<<HoTen<< " ngu ngay 8
tieng";}
void Xuat() const;
friend ostream& operator << (ostream &os, Nguoi& p);
};
Trang 141.4.Chương trình minh họa
class SinhVien : public Nguoi {
~SinhVien() {delete [] MaSo;}
void Xuat() const;
Trang 151.4.Chương trình minh họa
cout << "Nguoi, ho ten: " << HoTen
<< " sinh " << NamSinh;
}
void SinhVien::Xuat() const {
cout << "Sinh vien, ma so: " << MaSo
<< ", ho ten: " << HoTen;
}
Trang 161.4.Chương trình minh họa
void main() {
Nguoi p1(" Le Van Nhan",1980);
SinhVien s1("Vo Vien Sinh", "200002541",1984);
Trang 172.Kế thừa đơn
2.1.Khái niệm
- Kế thừa đơn cho phép một lớp là dẫn xuất của chỉ duy nhất 1 lớp cơ sở
- Kế thừa có thể được thực hiện để thể hiện mối
quan hệ 'là một'
Trang 182.3.Phương thức thiết lập và hủy
bỏ
• Phương thức thiết lập và huỷ bỏ là các hàm thành phần đặc
biệt dùng để tự động khởi động đối tượng khi nó được tạo ra và tự động dọn dẹp đối tượng khi nó bị hủy đi
• Một đối tượng thuộc lớp con có chứa các thành phần dữ liệu của các lớp cơ sở Có thể xem lớp con có các thành phần ngầm định ứng với các lớp cơ sở Vì vậy khi một đố tượng thuộc lớp con được tạo ra, các thành phần cơ sở cũng được tạo ra, nghĩa là phương thức thiết lập của các lớp cơ sở phải được gọi.
• Trình biên dịch tự động gọi phương thức thiết lập của các lớp
cơ sở cho các đối tượng (cơ sở) nhúng vào đối tượng đạng được tạo ra.
• Đối với phương thức thiết lập của một lớp con, công việc đầu tiên là gọi phương thức thiết lập của các lớp cơ sở.
Trang 192.3.Phương thức thiết lập và hủy bỏ
• Nếu mọi phương thức thiết lập của lớp cơ sở đều đòi hỏi phải cung cấp tham số thì lớp con bắt buộc phải có phương thức thiết lập để cung cấp các tham số đó
Trang 20Cung cấp tham số cho phương thức thiết lập của lớp cha
• Trong trường hợp đó, lớp con bắt buộc phải có phương thức thiết lập để cung cấp tham số cho phương thức thiết lập của lớp cơ sở Cú pháp để gọi phương thức thiết lập của lớp cơ
sở tương tự như cú pháp thiết lập đối tượng thành phần, bản thân tên lớp cơ sở được quan điểm như đối tượng thành
phần nhúng vào lớp con.
void Ve(int color) const;
void TinhTien(double dx, double dy) const;
};
HinhTron t(200,200,50); // Dung
Trang 212.3.Phương thức thiết lập và hủy bỏ
SinhVien(char *ht, char *ms, int ns) :
Nguoi(ht,ns) { MaSo = strdup(ms);
}
void Xuat() const;
};
Trang 222.3.Phương thức thiết lập và hủy
bỏ
• Sau khi phương thức thiết lập của các lớp cơ sở
được gọi, mã chương trình trong bản thân phương thức của lớp con sẽ được thực hiện Nội dung của phương thức thiết lập ở lớp con chỉ nên thao tác
trên dữ liệu của riêng lớp con, việc khởi động dữ liệu thuộc lớp cha do phương thức thiết lập ở lớp cha đảm nhiệm với các tham số cung cấp bởi lớp con
class SinhVien : public Nguoi
{
char *MaSo;
public:
SinhVien(char *ht, char *ms, int ns) :
Nguoi(ht,ns) { MaSo = strdup(ms); }
void Xuat() const;
};
Trang 232.3.Phương thức thiết lập và hủy bỏ
• Ta có thể khởi động các thành phần của lớp cha
bên trong phương thức thiết lập của lớp con Trong trường hợp này đối tượng thuộc lớp cha phải có khả năng tự khởi động:
class Complex {
protected:
double re, im;
public:
Complex(double r = 0, double i = 0):re(r), im(i){}
Complex operator +(Complex b);
Complex operator -(Complex b);
Complex operator *(Complex b);
Complex operator /(Complex b);
double Norm() const {return sqrt(re*re + im*im);}
Trang 242.3.Phương thức thiết lập và hủy bỏ
• Hai cách thiết lập đối tượng thuộc lớp con sau đây tương đương:
Trang 252.3.Phương thức thiết lập và hủy
Nguoi(char *ht = "Ng Van A", int ns = 1980)
:NamSinh(ns) {HoTen = strdup(ht);}
~Nguoi() {delete [] HoTen;}
//
};
Trang 262.3.Phương thức thiết lập và hủy bỏ
// Cach 1
class SinhVien : public Nguoi {
char *MaSo;
public:
SinhVien(char *ht, char *ms, int ns) :
Nguoi(ht,ns) { MaSo = strdup(ms); }
void Xuat() const;
SinhVien(char *ht, char *ms, int ns) {
HoTen = strdup(ht); MaSo = strdup(ms);
NamSinh = ns;
}
};
Trang 272.3.Phương thức thiết lập và hủy
bỏ
• Phương thức thiết lập sao chép là cần thiết
trong trường hợp đối tượng có nhu cầu cấp phát tài nguyên.
Trang 282.3.Phương thức thiết lập và hủy bỏ
Vấn đề: Ở lớp con có cần phương thức thiết lập sao chép ?
Trường hợp 1:
Không dùng phương thức thiết lập sao chép ở lớp con
class SinhVien : public Nguoi {
Trang 292.Phương thức thiết lập và hủy bỏ
Trường hợp 2:
Dùng phương thức thiết lập sao chép ở lớp con
class SinhVien : public Nguoi {
char *MaSo;
public:
SinhVien(char *ht, char *ms, int ns) :
Nguoi(ht,ns) { MaSo = strdup(ms);}
SinhVien(const SinhVien &s) : Nguoi(s)
Trang 302.3.Phương thức thiết lập và hủy bỏ
• Khi một đối tượng bị huỷ đi, phương thức huỷ bỏ của nó sẽ được gọi, sau đó phương thức huỷ bỏ của các lớp cơ sở sẽ được gọi một cách tự động Vì vậy lớp con không cần và cũng không được thực hiện các thao tác dọn dẹp cho các thành phần thuộc lớp cha.
class SinhVien : public Nguoi {
Trang 312.4 Sự tự động kế thừa các đặc tính của lớp cha
• Về mặt dữ liệu: Mỗi đối tượng sinh viên tự động có thành
phần dữ liệu họ tên và năm sinh của người.
• Về mặt thao tác: Lớp sinh viên được tự động kế thừa các
thao tác của lớp cha Đây chính là khả năng sử dụng lại mã chương trình.
• Riêng phương thức thiết lập không được kế thừa
Trang 322.4 Sự tự động kế thừa các đặc tính của lớp cha
Nguoi p1("Le Van Nhan",1980);
SinhVien s1("Vo Vien Sinh", "200002541",1984); p1.An(); cout << "\n";
s1.An(); cout << "\n"; // Tu lop Nguoi
p1.Xuat(); cout << "\n";
Trang 332.5.Định nghĩa lại thao tác ở lớp con
• Ta có thể định nghĩa lại các đặc tính ở lớp con đã có ở lớp cha, việc định nghĩa chủ yếu là thao tác, bằng cách khai báo giống hệt như ở lớp cha.
class SinhVien : public Nguoi {
void SinhVien::Xuat() const {
cout << "Sinh vien, ma so: " << MaSo << ", ho ten: " << HoTen;
}
Trang 34• Việc định nghĩa lại thao tác ở lớp con được thực hiện khi thao tác ở lớp con khác thao tác ở lớp cha Thông thường là các thao tác xuất, nhập.
• Ta cũng có thể định nghĩa lại thao tác ở lớp con trong trường hợp giải thuật ở lớp con đơn giản hơn (tô màu đa giác, tính modun của số ảo ).
class DaGiac {
//
void Ve() const;
void ToMau() const;
Trang 35• Hoặc ở lớp con, thao tác không có tác dụng
Trang 362.6.Ràng buộc ngữ nghĩa ở lớp
con
• Kế thừa có thể được áp dụng cho quan hệ kế thừa mang ý nghĩa ràng buộc, đối tượng ở lớp con là đối tượng ở lớp cha nhưng có dữ liệu bị ràng buộc.
– Hình tròn là Ellipse ràng buộc bán kính ngang dọc bằng nhau.
– Số ảo là số phức ràng buộc phần thực bằng 0.
– Hình vuông là hình chữ nhật ràng buộc hai cạnh ngang và dọc bằng nhau…
• Trong trường hợp này, các hàm thành phần phải bảo đảm sự ràng buộc dữ liệu được tôn trọng Lớp số ảo sau đây là một ví dụ minh hoạ.
Trang 372.6.Ràng buộc ngữ nghĩa ở lớp
con
class Complex {
friend ostream& operator <<(ostream&, Complex);
friend class Imag;
double re, im;
public:
Complex(double r = 0, double i = 0):re(r), im(i) {}
Complex operator +(Complex b);
Complex operator -(Complex b);
Complex operator *(Complex b);
Complex operator /(Complex b);
double Norm() const {return sqrt(re*re + im*im);} };
Trang 38class Imag: public Complex {
public:
Imag(double i = 0):Complex(0, i){}
Imag(const Complex &c) : Complex(0, c.im){} Imag& operator = (const Complex &c)
{re = 0; im = c.im; return *this;}
double Norm() const {return fabs(im);}
cout << "z1 = " << z1 << "\n";
cout << "i = " << i << "\n";
cout << "j = " << j << "\n";
}
Trang 392.6.Ràng buộc ngữ nghĩa ở lớp
class Imag: public Complex {
public:
//
Imag(const Complex &c) : Complex(0, c.im){}
Imag& operator = (const Complex &c)
{re = 0; im = c.im; return *this;}
};
Trang 402.7.Phạm vi truy xuất
• Khi thiết lập quan hệ kế thừa, ta vẫn ph i quan tâm đến ải quan tâm đến tính đóng gói và che dấu thông tin Điều này dẫn đến vấn đề xác định ảnh hưởng của kế thừa đến phạm vi truy xuất các thành phần của lớp Hai vấn đề được đặt ra là:
• Truy xuất theo chiều dọc: Hàm thành phần của lớp con có
quyền truy xuất các thành phần riêng tư của lớp cha hay không ? Vì chiều truy xuất là từ lớp con, cháu lên lớp cha nên ta gọi là truy xuất theo chiều dọc
• Truy xuất theo chiều ngang: Các thành phần của lớp cha,
sau khi kế thừa xuống lớp con, thì thế giới bên ngoài có quyền truy xuất thông qua đối tượng của lớp con hay
không ? Trong trường hợp này, ta gọi là truy xuất theo
chiều ngang.
Trang 417.1.Truy xuất theo chiều dọc
• Lớp con có quyền truy xuất các thành phần của lớp cha hay không, hay tổng quát hơn, nơi nào có quyền truy xuất các thành phần của lớp cha, hoàn toàn do lớp cha quyết định Điều đó được xác định bằng
thuộc tính kế thừa
• Trong trường hợp lớp sinh viên kế thừa từ lớp người, truy xuất theo chiều dọc có nghĩa liệu lớp sinh viên có quyền truy xuất các thành phần họ tên, năm sinh của lớp người hay không Chính xác hơn một đối
tượng sinh viên có quyền truy xuất họ tên của chính mình nhưng được khai báo ở lớp người hay không?
Trang 422.7.1.Truy xuất theo chiều dọc
• Thuộc tính kế thừa là đặc tính của một thành phần của lớp
cho biết những nơi nào có quyền truy xuất thành phần đó.
- Thuộc tính public: Thành phần nào có thuộc tính public thì
có thể được truy xuất từ bất cứ nơi nào (từ sau khai báo
lớp).
- Thuộc tính private: Thành phần nào có thuộc tính private
thì nó là riêng tư của lớp đó Chỉ có các hàm thành phần của lớp và ngoại lệ là các hàm bạn được phép truy xuất, ngay cả các lớp con cũng không có quyền truy xuất.
- Thuộc tính protected: cho phép qui định một vài thành
phần nào đó của lớp là bảo mật, theo nghĩa thế giới bên ngoài không được phép truy xuất, nhưng tất cả các lớp con, cháu… đều được phép truy xuất
Trang 44Thuộc tính private
• Trong ví dụ trên, không có hàm thành phần nào của lớp
SinhVien có thể truy xuất các thành phần private HoTen,
NamSinh của lớp Nguoi Nói cách khác, lớp con không có quyền vi phạm tính đóng gói của lớp cha Đoạn chương trình sau gây ra lỗi lúc biên dịch.
void SinhVien::Xuat() const {
cout << "Sinh vien, ma so: " << MaSo << ", ho ten: " << HoTen;
}
• Ta có thể khắc phục được lỗi trên nhờ khai báo lớp SinhVien là bạn của lớp Nguoi, như trong ví dụ ở đầu chương:
Trang 46Thuộc tính private
• Với khai báo hàm bạn như trên, lớp sinh viên có thể truy xuất các thành phần của lớp người
void SinhVien::Xuat() const {
cout << "Sinh vien, ma so: " << MaSo
<< ", ho ten: " << HoTen; // Ok }
• Cách làm trên giải quyết được nhu cầu của người sử dụng khi muốn tạo lớp con có quyền truy xuất các thành phần dữ liệu private của lớp cha Tuy nhiên nó đòi hỏi phải sửa đổi lại lớp cha và tất cả các lớp ở cấp cao hơn mỗi khi một lớp con mới ra đời
Trang 47Thuộc tính private
class Nguoi {
friend class SinhVien;
friend class NuSinh;
class SinhVien : public Nguoi {
friend class NuSinh;
char *MaSo;
public:
//
};
Trang 48Nguoi p1("Le Van Nhan",1980);
SinhVien s1("Vo Vien Sinh", "200002541",1984);
NuSinh ns("Le Thi Ha Dong", "200002544",1984);
p1.An(); cout << "\n";
s1.An();cout << "\n";
ns.An();cout << "\n";
}
Trang 50~SinhVien() {delete [] MaSo;}
void Xuat() const; // Co the truy xuat
// Nguoi::HoTen va Nguoi::NamSinh };
class NuSinh : public SinhVien {
Trang 51Thuộc tính protected
void Nguoi::Xuat() const {
cout << "Nguoi, ho ten: " << HoTen << " sinh " << NamSinh;
}
void SinhVien::Xuat() const {
cout << "Sinh vien, ma so: " << MaSo << ", ho ten:
" << HoTen; // Ok: co quyen truy xuat
// Nguoi::HoTen, Nguoi::NamSinh }
void SinhVien::Xuat() const {
cout << "Sinh vien, ma so: " << MaSo
<< ", ho ten: " << HoTen;
}
Trang 52Thuộc tính protected
• Thuộc tính protected là phương tiện để tránh phải sửa đổi lớp cơ sở khi có lớp con mới ra đời Nhờ đó nó bảo được tính đóng của một lớp Khai báo một thành phần nào có thuộc tính protected tương
đương với qui định trước tất cả các lớp con, cháu sau này đều là bạn của thành phần đó
• Thông thường ta dùng thuộc tính protected cho các thành phần dữ liệu và thuộc tính public cho hàm thành phần
• Các thuộc tính public, private, protected và khai
báo friend cho những nơi nào có quyền truy xuất đến các thành phần của lớp Cho hay không cho ai truy xuất đến (thành phần của) lớp hoàn toàn do lớp quyết định