Bài giảng lập trình hướng đối tượng - Thầy Cường Học viện bưu chính viễn thông TP HCM
Trang 1Chương 6
Tính kế thừa
• Giới thiệu tính kế thừa
• Điều khiển truy cập lớp cơ sở
• Sử dụng các thành viên được bảo vệ
• Hàm tạo, hàm hủy và tính kế thừa
• Tính đa kế thừa
• Lớp cơ sở ảo
Trang 3I/ Giới thiệu tính kế thừa (inheritance)
Tính kế thừa là cơ chế nhờ đó một lớp có thể kế thừa các đặc điểm của một lớp khác
Tính kế thừa hổ trợ khái niệm phân loại theo thứ bậc (hierachical classification) của lớp, ngoài ra còn hổ trợ tính đa hình (polymorphism)
• Lớp cơ sở (base class) là lớp được kế thừa bởi một lớp khác
Lớp dẫn xuất (derive class) là lớp kế thừa từ một lớp cơ sở
Lớp cơ sở xác định các tính chất mà sẽ trở nên thông dụng cho các lớp dẫn xuất Nghiã là lớp cơ sở hiển thị mô tả tổng quát nhất một tập hợp các đặc điểm
Một lớp dẫn xuất kế thừa các đặc điểm tổng quát này và bổ sung thêm các tính chất riêng của lớp dẫn xuất
Cú pháp khai báo cho lớp dẫn xuất
class derived_class_name : access_specifier base_class_name {
// body of class
} ;
base_class_name Tên lớp cơ sở
derived_class_name Tên lớp dẫn xuất
access_specifier chỉ định truy cập bao gồm : public, private và protected
• Từ khoá public báo cho trình biên dịch biết rằng lớp cơ sở sẽ được kế thừa sao
cho mọi thành viên chung của lớp cơ sở cũng sẽ là các thành viên chung của lớp
dẫn xuất Tuy nhiên, mọi thành viên riêng của lớp cơ sở vẫn còn riêng đối với nó
và không được truy cập trực tiếp bởi lớp dẫn xuất
Trang 4// Define base class
// Define derived class
class derived : public base {
// Return value of i in base
int base::get_i() { return i; }
// Set value of j in derived
Trang 5int main()
{
derived ob;
ob.set_i(10); // load i in base
ob.set_j(4); // load j in derived
cout << ob.mul(); // displays 40
return 0;
}
• Một lớp cơ sở là không thuộc riêng về một lớp dẫn xuất Lớp cơ sở có thể được kế
thừa bởi nhiều lớp khác
Ví dụ 1.2 Lớp cơ sở chung Fruit có 2 lớp dẫn xuất Apple và Orange
// An example of class inheritance
#include <iostream.h.h>
#include <string.h>
enum yn {no, yes};
enum color {red, yellow, green, orange};
void out(enum yn x);
char *c[] = {"red", "yellow", "green", "orange"};
// Generic fruit class
Trang 6// Derive Apple class
class Apple : public fruit {
// Derive orange class
class Orange : public fruit {
Trang 7cout << name << " apple is: " << "\n";
cout << "Annual: "; out(annual);
cout << "Perennial: "; out(perennial);
cout << "Tree: "; out(tree);
cout << "Tropical: "; out(tropical);
cout << "Color: " << c[clr] << "\n";
cout << "Good for cooking: "; out(cooking);
cout << "Crunchy: "; out(crunchy);
cout << "Good for eating: "; out(eating); cout << "\n";
}
void Orange::show()
{
cout << name << " orange is: " << "\n";
cout << "Annual: "; out(annual);
cout << "Perennial: "; out(perennial);
cout << "Tree: "; out(tree);
cout << "Tropical: "; out(tropical);
cout << "Color: " << c[clr] << "\n";
cout << "Good for juice: "; out(juice);
cout << "Sour: "; out(sour);
cout << "Good for eating: "; out(eating); cout << "\n";
}
Trang 8void out(enum yn x)
{
if(x==no) cout << "no\n";
else cout << "yes\n";
}
int main()
{
Apple a1, a2;
Orange o1, o2;
a1.seta("Red Delicious", red, no, yes, yes);
a2.seta("Jonathan", red, yes, no, yes);
o1.seto("Navel", orange, no, no, yes);
o2.seto("Valencia", orange, yes, yes, no);
Trang 9II/ Điều khiển truy cập lớp cơ sở
Chỉ định truy cập (access_specifier) xác định cách mà các phần tử của lớp cơ sở
được kế thừa bởi lớp dẫn xuất
• Từ khoá private chỉ định các thành viên chung lớp cơ sở trở thành các thành
viên riêng của lớp dẫn xuất, nhưng những thành viên này vẫn còn được truy cập bởi các hàm thành viên của lớp dẫn xuất
ob.setx(10); // access member of base class
ob.sety(20); // access member of derived class
ob.showx(); // access member of base class
ob.showy(); // access member of derived class
Trang 10// Inherit as public - this has an error!
class derived : public base {
int y;
public:
void sety(int n) { y = n; }
/* Cannot access private member of base class
x is a private member of base and not available within derived */
void show_sum() { cout << x + y << '\n'; } // Error!
void showy() { cout << y << '\n'; }
};
Ví dụ 2.3 Lớp dẫn xuất kế thừa lớp cơ sở với chỉ định private
// This program contains an error
Trang 11// Inherit base as private
class derived : private base {
ob.setx(10); // ERROR - now private to derived class
ob.sety(20); // access member of derived class - OK
ob.showx(); // ERROR - now private to derived class
ob.showy(); // access member of derived class - OK
base_ob.setx(1); // is legal because base_ob is of type base
Ví dụ 2.4 Lớp dẫn xuất kế thừa lớp cơ sở với chỉ định private, các thành viên vẫn
còn được truy cập bên trong lớp dẫn xuất
// This program is fixed
Trang 12// Inherit base as private
class derived : private base {
int y;
public:
// setx is accessible from within derived
void setxy(int n, int m) { setx(n); y = m; }
// showx is accessible from within derived
void showxy() { showx(); cout << y << '\n'; }
Trang 13void setab(int i, int j) { a = i; b = j; }
void getab(int &i, int &j) { i = a; j = b; }
Trang 14III/ Sử dụng các thành viên được bảo vệ (protected members)
Với các chỉ định truy cập public và private, một lớp dẫn xuất không truy cập được
các thành viên riêng của lớp cơ sở Tuy nhiên, sẽ có những lúc muốn một thành viên của lớp cơ sở vẫn là riêng nhưng vẫn cho phép lớp dẫn xuất truy cập tới nó
• Chỉ định truy cập protected, tương đương với chỉ định private với một ngoại lệ
duy nhất là các thành viên được bảo vệ (protected members) của lớp cơ sở có thể
được truy cập đối với các thành viên của một lớp dẫn xuất từ lớp cơ sở đó
Bên ngoài lớp cơ sở hoặc lớp dẫn xuất, các thành viên được bảo vệ không thể
được truy cập
Dạng tổng quát của khai báo lớp :
Khi một thành viên được bảo vệ của một lớp cơ sở được kế thừa bởi lớp dẫn xuất với
chỉ định public, nó trở thành thành viên được bảo vệ của lớp dẫn xuất
Nếu lớp dẫn xuất được kế thừa với chỉ định private, thì một thành viên được bảo vệ
của lớp cơ sở trở thành thành viên riêng của lớp dẫn xuất
Lớp cơ sở có thể được kế thừa với chỉ định protected bởi lớp dẫn xuất, khi đó các
thành viên chung và được bảo vệ của lớp cơ sở trở thành các thành viên được bảo vệ của lớp dẫn xuất
Còn các thành viên riêng của lớp cơ sở vẫn còn riêng đối với lớp cơ sở và không được truy cập bởi lớp dẫn xuất
Trang 15Ví dụ 3.1 Truy cập các thành viên chung, riêng và được bảo vệ của lớp cơ sở
int geta() { return a; }
int getb() { return b; }
};
int main()
{
samp ob(10, 20);
// ob.b = 99; Error! b is protected and thus private
ob.c = 30; // OK, c is public
protected : // private to base
int a, b; // but still accessible by derived
public:
Trang 16void setab(int n, int m) { a = n; b = m; }
Ví dụ 3.3 Các thành viên được bảo vệ được kế thừa bởi chỉ định protected
// This program will not compile
#include <iostream.h>
class base {
protected : // private to base
int a, b; // but still accessible by derived
public:
Trang 17void setab(int n, int m)
// ERROR: setab() is now a protected member of base
ob.setab(1, 2); // setab() is not accessible here
ob.setc(3);
ob.showabc();
return 0;
}
Trang 18Bài tập III
1 Lập bảng tổng kết về quyền truy cập của lớp dẫn xuất với sự kế thừa đơn
Thuộc tính thành viên
trong lớp cơ sở Chỉ định truy cập của lớp dẫn xuất Quyền truy cập của lớp dẫn xuất Public Public
Private Protected
?
?
? Private Public
Private Protected
?
?
? Protected Public
Private Protected
Trang 19IV/ Hàm tạo, hàm hủy và tính kế thừa
Lớp cơ sở, lớp dẫn xuất hoặc cả hai có thể có các hàm tạo và/hoặc hàm hủy
• Khi cả lớp cơ sở lẫn lớp dẫn xuất có các hàm tạo và hàm hủy, các hàm tạo được thi hành theo thứ tự dẫn xuất Cáùc hàm hủy được thi hành theo thứ tự ngược lại
Cú pháp truyền đối số từ lớp dẫn xuất đến lớp cơ sở :
base() { cout << "Constructing base class \n"; }
~base() { cout << "Destructing base class \n"; }
};
class derived : public base {
public:
derived() { cout << "Constructing derived class \n"; }
~derived() { cout << "Destructing derived class \n"; }
Trang 20Constructing base class
Constructing derived class
Destructing derived class
Destructing base class
Ví dụ 4.2 Truyền một đối số cho hàm tạo của lớp dẫn xuất
#include <iostream.h>
class base {
public:
base() { cout << "Constructing base class\n"; }
~base() { cout << "Destructing base class\n"; }
~derived() { cout << "Destructing derived class\n"; }
void showj() { cout << j << '\n'; }
Kết quả của chương trình ?
Ví dụ 4.3 Hàm tạo của lớp dẫn xuất lẫn của lớp cơ sở nhận một đối số,
Trang 21cả hai sử dụng đối số giống nhau
~base() { cout << "Destructing base class\n"; }
void showi() { cout << i << '\n'; }
};
class derived : public base {
int j;
public:
derived(int n) : base(n) { // pass arg to base class
cout << "Constructing derived class\n";
j = n;
}
~derived() { cout << "Destructing derived class\n"; }
void showj() { cout << j << '\n'; }
Trang 22• Trong hầu hết các trườnghợp, các hàm tạo đối với lớp cơ sở và lớp dẫn xuất sẽ
không dùng đối số giống nhau
Nếu truyền một hay nhiều đối số cho mỗi lớp, cần phải truyền cho hàm tạo của lớp
dẫn xuất tất cả các đối số mà cả hai lớp dẫn xuất và lớp cơ sở cần đến Sau đó lớp
dẫn xuất chỉ truyền cho lớp cơ sở những đối số nào mà lớp cơ sở cần đến
~base() { cout << "Destructing base class\n"; }
void showi() { cout << i << '\n'; }
};
class derived : public base {
int j;
public:
derived(int n, int m) : base(m) { // pass arg to base class
cout << "Constructing derived class\n";
j = n;
}
~derived() { cout << "Destructing derived class\n"; }
void showj() { cout << j << '\n'; }
};
int main()
{
derived o(10, 20);
Trang 23o.showi();
o.showj();
return 0;
}
Kết quả của chương trình ?
• Đối với hàm tạo của lớp dẫn xuất không cần phải nhận một số để truyền cho lớp cơ sở Nếu lớp dẫn xuất không cần đối số, nó bỏ qua đối số và chỉ truyền cho lớp cơ sở
~base() { cout << "Destructing base class\n"; }
void showi() { cout << i << '\n'; }
};
class derived : public base {
int j;
public:
derived(int n) : base(n) { // pass arg to base class
cout << "Constructing derived class\n";
j = 0; // n not used here
}
~derived() { cout << "Destructing derived class\n"; }
void showj() { cout << j << '\n'; }
};
Bài tập IV
Trang 241 Cho đoạn chương trình sau, hãy bổ sung hàm tạo cho lớp myderived Cho myderived truyền cho lớp mybase một con trỏ về một chuỗi khởi đầu Hãy cho hàm tạo myderived() khởi đầu len với độ dài chuổi
// add myderived() here
int getlen() { return len; }
void show() { cout << get() << '\n'; }
Trang 25car ob(passengers, wheels, range) ;
truck ob(loadlimit, wheels, range) ;
cout << "Wheels: " << num_wheels << '\n';
cout << "Range: " << range << '\n';
Trang 271/ Khái niệm
• Có hai cách để một lớp dẫn xuất kế thừa hơn một lớp
Thứ nhất, lớp dẫn xuất được dùng như một lớp cơ sở cho một lớp dẫn xuất khác,
tạo ra thứ bậc lớp nhiều cấp Khi đó cấp cơ sở gốc được gọi là lớp cơ sở giáng
tiếp của lớp cơ sở thứ hai (Hình 6.2)
Hình 6.2 Kế thừa đa mức Hình 6.3 Kế thừa phân cấp
(Multi level Inheritance) (Hierarchical Inheritance)
Thứ hai, lớp dẫn xuất có thể kế thừa trực tiếp hơn một lớp cơ sở Trường hợp này, hai hay nhiều lớp cơ sở được kết hợp để tạo ra lớp dẫn xuất (Hình 6.4)
Hình 6.4 Đa kế thừa Hình 6.5 Kế thừa lai
(Multiple Inheritance) (Hybrid Inheritance)
2/ Tính chất
• Khi một lớp cơ sở được dùng để dẫn ra một lớp mà lớp này lại được dùng làm
lớp cơ sở cho một lớp dẫn xuất khác, thì các hàm tạo của các lớp được gọi theo
thứ tự dẫn xuất Cũng vậy, tất cả các hàm hủy được gọi theo thứ tự ngược lại
• Khi một lớp dẫn xuất kế thừa trực tiếp hơn một lớp cơ sở, nó dùng cách khai báo mở rộng như sau :
class derived_class_name : access base1 , access base2, , access baseN {
Trang 28}
base1 , base2, , baseN tên các lớp cơ sở
access chỉ định truy cập, có thể khác nhau đối với mỗi lớp
Khi nhiều lớp cơ sở được kế thừa, các hàm tạo được thi hành theo thứ tự từ trái
qua phải mà các lớp cơ sở đã được chỉ định rõ Các hàm hủy được thi hành
theo thứ tự ngược lại
• Khi một lớp kế thừa nhiều lớp cơ sở có các hàm tạo cần nhiều đối số, lớp dẫn xuất sẽ truyền các đối số cần thiết cho các hàm tạo này bằng cách dùng dạng mở rộng của hàm tạo của lớp dẫn xuất như sau :
derived_constructor(arg_list) : base1(arg_list) , base2(arg_list), , baseN(arg_list) {
// body of class
}
Khi một lớp dẫn xuất kế thừa một hệ thống thứ bậc các lớp, mỗi lớp dẫn xuất trong chuỗi các lớp này phải truyền cho lớp cơ sở đứng trước các tham số mà lớp dẫn xuất này cần
Trang 29// geta() and getb() are still public here
cout << ob.geta() << ' ' << ob.getb() << '\n';
return 0;
}
Kết quả của chương trình ?
Ví dụ 5.2 Lớp dẫn xuất kế thừa trực tiếp hai lớp cơ sở
#include <iostream.h>
Trang 30// Create first base class
// Directly inherit two base classes
class D : public B1, public B2 {
int c;
public:
// here, z and y are passed directly to B1 and B2
D(int x, int y, int z) : B1(z) , B2(y) { c = x; }
// Because bases inherited as public, D has access to public elements
Trang 31Kết quả của chương trình ?
Ví dụ 5.3 Minh họa thứ tự gọi các hàm tạo và hàm hủy khi lớp dẫn xuất
kế thừa trực tiếp nhiều lớp cơ sở
// Inherit two base classes
class D : public B1 , public B2 {
Trang 32A() { cout << "Constructing A\n"; }
~A() { cout << "Destructing A\n"; }
Trang 332 Sử dụng hệ thống thứ bậc lớp sau đây, hãy lập hàm tạo cua lớp C để cho nó khởi đầu k và truyền các đối số cho A() và B()
Trang 34Khi nhiều lớp cơ sở được kế thừa trực tiếp bởi một lớp dẫn xuất, tồn tại một vấn đề : xuất hiện nhiều bản sao của lớp cơ sở cùng hiện diện trong đối tượng dẫn xuất Chẳng hạn, xét hệ thống thứ bậc lớp sau
Bời vì có hai bản sao của B trong D3, nên một tham chiếu đến một thành viên của B sẽ tham chiếu về B được kế thừa gián tiếp thông qua D1 hay tham chiếu về B được kế thừa gián tiếp thông qua D2 ?
Để giải quyết tính không rõ ràng này, C++ có một cơ chế mà nhờ đó chỉ có một bản
sao của B ở trong D3 Đặc điểm này được gọi là lớp cơ sở ảo
Có thể ngăn chặn được hai bản sao của lớp cơ sở cùng hiện diện trong đối tượng dẫn
xuất bằng cách cho lớp cơ sở đó được kế thừa như virtual bởi bất kỳ các lớp dẫn
xuất nào
• Từ khoá virtual đứng trước chỉ định truy cập lớp cơ sở khi nó được kế thừa bởi
một lớp dẫn xuất
D3