Mời các bạn tham khảo Bài giảng Lập trình hướng đối tượng - Chương 3: Kế thừa sau đây để bổ sung thêm các kiến thức về kế thừa đơn, phạm vi truy xuất, phương thức thiết lập và hủy bỏ, con trỏ và kế thừa.
Trang 31 Mở đầu
- Sự kế thừa là một đặc điểm của ngôn ngữ dùng để biểudiễn mối quan hệ đặc biệt giữa các lớp Các lớp được trừutượng hóa và tổ chức thành một sơ đồ phân cấp lớp
- Kế thừa là một cơ chế trừu tượng hóa Thủ tục và hàm là cơchế trừu tượng hóa cho giải thuật, record và struct là trừutượng hóa cho dữ liệu Khái niệm lớp trong C++, kết hợpdữ liệu và thủ tục để được kiểu dữ liệu trừu tượng với giao
dữ liệu và thủ tục để được kiểu dữ liệu trừu tượng với giaodiện độc lập với cài đặt và cho người sử dụng cảm giácthoải mái như kiểu dữ liệu có sẵn
- Sự kế thừa là một mức cao hơn của trừu tượng hóa cungcấp một cơ chế gom chung các lớp có liên quan với nhauthành một mức khái quát hóa đặc trưng cho toàn bộ các lớpnói trên Các lớp với các đặc điểm tương tự nhau có thể
Trang 4Mở đầu
Quan hệ là 1: Kế thừa được sử dụng thông dụng nhất để biểu diễn quan hệ là một
• Một sinh viên là một người
• Một hình tròn là một hình ellipse
• Một tam giác là một đa giác
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
đó 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ệ
Trang 5Mở đầu
Kế thừa thường được dùng theo hai cách:
• Để phản ánh mối quan hệ giữa các lớp Là công cụ để tổ chức và phân cấp lớp dựa vào sự chuyên biệt hóa, trong đó một vài hàm thành phần của lớp con là phiên bản hoàn thiện hoặc đặc biệt hoá của phiên bản ở lớp cha Trong C++ mối quan hệ này thường được cài đặt sử dụng:
sử dụng:
Kế thừa public.
Hàm thành phần là phương thức ảo
• Để phản ánh sự chia sẻ mã chương trình giữa các lớp không có quan hệ về mặt ngữ nghĩa nhưng có thể có tổ chức dữ liệu và mã chương trình tương tự nhau Trong
Trang 62 Kế thừa đơn
Kế thừa có thể được thực hiện để thể hiện mối quan hệ 'là một'
Xét hai khái niệm người và sinh viên với mối quan hệ tựnhiên: một 'sinh viên' là một 'người' Trong C++, ta có thểbiểu diễn khái niệm trên, một sinh viên là một người cóthêm một số thông tin và một số thao tác (riêng biệt củasinh viên)
sinh viên)
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ẫnxuất (derived class)
Trang 7Kế thừa đơn
~Nguoi() {delete [] HoTen;}
void An() const { cout << HoTen << " an 3 chen com";}
void Ngu() const { cout << HoTen << " ngu ngay
8 tieng";}
void Xuat() const;
Trang 8Kế thừa đơn
class SinhVien : public Nguoi
{
char *MaSo;
public:
SinhVien(char *ht, char *ms, int ns) :
Nguoi(ht,ns) { MaSo = strdup(ms);}
~SinhVien() {delete [] MaSo;}
void Xuat() const;
void Xuat() const;
Trang 9Kế thừa đơn
void Nguoi::Xuat() const
Trang 10Kế thừa đơn
void main()
{
Nguoi p1("Le Van Nhan",1980);
SinhVien s1("Vo Vien Sinh", "200002541",1984);
Trang 11Tự động kế thừa các đặc tính của lớp cha
Cho biết lớp sinh viên kế thừa từ lớp người Khi đó sinh
viên được thừa hưởng các đặc tính của lớp người
viên được thừa hưởng các đặc tính của lớp người
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
Trang 12Tự độ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);
Trang 13Đị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
Trang 14Việ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 )
void Ve() const;
void ToMau() const;
Trang 15Định nghĩa lại thao tác ở lớp con
Hoặc ở lớp con, thao tác không có tác dụng
Trang 163 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
• 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 17Ràng buộc ngữ nghĩa ở lớp con
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 18class 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);}
Trang 19Ràng buộc ngữ nghĩa ở lớp con
Trong ví dụ trên lớp số ảo (Imag) kế thừa hầu hết các thao tác của lớp số phức (Complex) Tuy nhiên ta muốn ràng buộc mọi đối tượng thuộc lớp số ảo đều phải có phần thực bằng 0 Vì vậy phải định nghĩa lại các hàm thành phần có thể vi phạm điều này Ví dụ phép toán gán phải được định nghĩa lại để bảo đảm ràng buộc này
class Imag: public Complex
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 20Ràng buộc ngữ nghĩa ở lớp con
Ví dụ sau minh hoạ thêm ràng buộc ngữ nghĩa ở lớp con
class HCN:public Hinh
HCN(Diem tt, double r, double c);
HCN(Diem tt, double r, double c);
HCN(double ttx, double tty, double r, double c); HCN():TrenTrai(4,6), rong(7), cao(4){}
double DienTich() const {return rong*cao;}
void Nhap() {cin >> TrenTrai >> rong >> cao;}
void Xuat();
void PhongTo(double tiLe);
void GianNgang(double tiLe);
void GianDoc(double tiLe);
Trang 21Ràng buộc ngữ nghĩa ở lớp con
void HCN::PhongTo(double tiLe)
Trang 22Ràng buộc ngữ nghĩa ở lớp con
class HV:public HCN
{
public:
HV(Diem tt, double canh):HCN(tt, canh, canh){}
HV(double ttx, double tty, double
canh):HCN(ttx,tty,canh,canh){}
HV():HCN(7,8,6,6){}
char *TenLop() {return "Hinh Vuong";}
char *TenLop() {return "Hinh Vuong";}
void Nhap() {cin >> TrenTrai >> rong; cao = rong;} void Xuat();
void GianNgang(double tiLe);
void GianDoc(double tiLe);
};
Trang 23Ràng buộc ngữ nghĩa ở lớp con
void HV::GianNgang(double tiLe)
Trang 244 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 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
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 254.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 truy xuất
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
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?
Thuộc tính truy xuất 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
Trang 26Thuộc tính truy xuất
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
ngay cả các lớp con cũng không có quyền truy xuất
Trang 27Thuộc tính truy xuất
class SinhVien : public Nguoi
class SinhVien : public Nguoi
Trang 28Thuộ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
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 29class SinhVien : public Nguoi
class SinhVien : public Nguoi
{
char *MaSo;
public:
//
void Xuat() const { cout << "Sinh vien, ma so: "
<< MaSo << ", ho ten: " << HoTen; }
Trang 30Cá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 31Thuộc tính private
class Nguoi
{
friend class SinhVien;
friend class NuSinh;
Trang 32Nguoi 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";
Trang 33Thuộc tính protected
Trong ví dụ trên, khi lớp NuSinh ra đời ta phải thay đổi lớp cha SinhVien và cả lớp cơ sở Nguoi ở mức cao hơn
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 34~SinhVien() {delete [] MaSo;}
~SinhVien() {delete [] MaSo;}
void Xuat() const; // Co the truy xuat
// Nguoi::HoTen va Nguoi::NamSinh };
Trang 35}; // Co the truy xuat Nguoi::HoTen va
}; // Co the truy xuat Nguoi::HoTen va
// Nguoi::NamSinh va SinhVien::MaSo
Trang 37Thuộ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
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
Trang 384.2 Truy xuất theo chiều ngang
Thành phần protected và public của lớp 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 thuộc lớp con hay không? Điều này hoàn toàn do lớp con quyết định bằng thuộc tính kế thừa Có hai thuộc tính kế thừa là kế thừa public và kế thừa
private
Kế thừa public: Lớp con kế thừa public từ lớp cha thì các
Kế thừa public: Lớp con kế thừa public từ lớp cha thì các thành phần protected của lớp cha trở thành protected của lớp con, các thành phần public của lớp cha trở thành public của lớp con Nói cách khác mọi thao tác của lớp cha được kế thừa xuống lớp con Vì vậy ta có thể sử dụng thao tác của lớp cha cho đối tượng thuộc lớp con
Ta qui định kế thừa public bằng từ khoá public theo sau
Trang 39Kế thừa public
class SinhVien : public Nguoi
{
char *MaSo;
public:
SinhVien(char *ht, char *ms, int ns) :
Nguoi(ht,ns) { MaSo = strdup(ms);}
~SinhVien() {delete [] MaSo;}
void Xuat() const;
void Xuat() const;
Trang 40Kế thừa public
Do được thừa hưởng các đặc tính của lớp cha nên ta dùng kế thừa public khi và chỉ khi có quan hệ là một từ lớp con đến lớp cha
Hầu hết các trường hợp kế thừa là kế thừa public, nó cho phép tận dụng lại mã chương trình, đồng thời tạo khả năng thu gom các đặc điểm chung của các lớp vào một lớp cơ sở, nhờ đó dễ dàng nâng cấp và sửa chữa (bảo trì)
sở, nhờ đó dễ dàng nâng cấp và sửa chữa (bảo trì)
Trang 41Kế thừa private
Có những trường hợp các lớp không có quan hệ với nhau về mặt ngữ nghĩa nhưng chia sẻ chung chi tiết cài đặt, nếu dùng kế thừa public thì sai khái niệm vì lớp con sẽ thừa
hưởng các thao tác nó không có từ lớp cha
Kế thừa private: Lớp con kế thừa private từ lớp cha thì các thành phần protected và public của lớp cha trở thành
private của lớp con Nói cách khác mọi thao tác của lớp
private của lớp con Nói cách khác mọi thao tác của lớp
cha đều bị lớp con che dấu Vì vậy trên quan điểm của thế giới bên ngoài lớp con không có các thao tác mà lớp cha có
Sử dụng kế thừa private Ta có thể chia sẻ mã chương trình giữa các lớp có cấu trúc dữ liệu tương tự nhau nhưng vẫn
Trang 42Kế thừa private
Ví dụ sau minh hoạ kế thừa private: Lớp List biểu diễn
khái niệm danh sách liên kết, lớp Stack biểu diễn , lớp Set biểu diễn khái niệm tập hợp Nếu tổ chức Stack, Set như danh sách liên kết, có thể dùng kế thừa private để tận
dụng mã chương trình chung
typedef int Item;
typedef int bool;
typedef int bool;
const bool true = 1, false = 0;
class Link
{
friend class List;
friend class ListIterator;
Link *next;
Item e;
Link(Item a, Link *p):e(a) {next = p;}
Trang 43Kế thừa private
void Insert(Item a); // add at head of list
void Insert(Item a); // add at head of list
void Append(Item a); // add at tail of list
void GetFirst(Item *px); // return and remove head void CleanUp();
bool Empty() const {return last == NULL;}
bool IsMember(Item x) const;
int Count() const;
Trang 44Kế thừa private
class Stack:private List
Trang 45Kế thừa private
class Set:private List
bool Empty() const {return List::Empty();}
bool IsMember(Item x) const {return
bool IsMember(Item x) const {return
List::IsMember(x);}
int Count() const {return List::Count();}
void View() const {List::View();}
};
Các lớp Stack và Set tận dụng được cấu trúc dữ liệu và chi tiết cài đặt của lớp List, nhưng không bị trở thành List vì sử
Trang 46Kế thừa private
Một số hàm thành phần của lớp cơ sở List có thể cần thiết
ở lớp con như hàm Empty trong lớp Stack, hàm Empty,
IsMember, Count trong lớp Set… Ta định nghĩa lại những hàm này bằng cách gọi lại phiên bản trong lớp List
Một cách thay thế việc viết lại hàm như trên là khai báo lại các danh hiệu này trong phần public của lớp con
class Stack:private List
class Stack:private List
Trang 47Kế thừa private
Ta có thể làm tương tự cho lớp Set
class Set:private List
List::Empty; // access specifier
List::Empty; // access specifier