5 .3| Cerr và clog
5.4 .3| Đặc tính của kế thừa
Kế thừa cho phép định nghĩa lớp mới từ lớp đã có, khi đó:
• Lớp mới được gọi là lớp con (subclass) hay lớp dẫn xuất (derived class). • Lớp ban đầu gọi là lớp cha (superclass) hay lớp cơ sở (base class). Lớp std::ofstream được phát triển từ lớp ostream có sẵn nên nó được xem như một loại ostream đặc biệt. Trong ví dụ sau ta có thể quan sát một cơ chế kế thừa của class trong C++:
Giả sử lớp B được định nghiã như sau: class B {
// bỏ qua phần chi tiết };
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 92
Ta khai báo lớp dẫn xuất D từ lớp B theo cú pháp sau: class D: public B {
// Details omitted };
B là base class và D là derived class. B là lớp có trước và D là lớp mới được tạo sau từ B
5.4.4| TỔNG QUÁT HĨA, ĐẶC BIỆT HĨA
Tổng qt hóa (Generalization)
Là việc trích ra những đặt tính chung nhất từ hai hoặc nhiều lớp để tạo nên lớp cha (lớp tổng quát)
Đặc biệt hóa (Specialization):
Là tạo một lớp con từ một lớp đã tồn tại (mối quan hệ is_a)
5.4.5| CÚ PHÁP KHAI BÁO KẾ THỪA
Cú pháp khai báo kế thừa như sau: class SuperClass
{
//Thành phần của lớp cơ sở
};
class DerivedClass : public/protected/private
SusperClass
{
//Thành phần bổ sung của lớp dẫn xuất
};
5.4.6| TẦM VỰC TRONG KẾ THỪA
Khi cài đặt kế thừa trong lập trình hướng đối tượng người ta vẫn phải quan tâm đến tính đóng gói và che giấu thông tin. Điều này ảnh hưởng đến phạm vi truy xuất các thành phần trong class. Một lớp kế thừa (Derived Class) sẽ kế thừa tất cả các phương thức của lớp cơ sở, ngoại trừ:
• Constructor, destructor và copy constructor của lớp cơ sở. • Overloaded operator (toán tử nạp chồng) của lớp cơ sở.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 93
− 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 của lớp cha hay không?
Truy xuất theo chiều dọc cụ thể ở trên lớp dẫn xuất (derived class) chỉ có thể truy cập các thành phần Public và Protected của lớp cơ sở (base class). Còn các hàm bạn, lớp bạn thì có thể truy cập tất cả. Những hàm hoặc lớp khơng có mối quan hệ với đó thì chỉ sử dụng được các thành viên public.
−
− 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 ngồi có quyền truy xuất thơng qua đối tượng của lớp con hay không?
Truy xuất theo chiều ngang có nghĩa là việc lớp con quyết định kế thừa theo public, protected hay private có ảnh hưởng đến việc truy cập các thành phần của class cha.
Kiểu kế thừa Public: Trong trường một một sub class được dẫn xuất từ một public base class thì các thành viên public của base class trở thành thành viên public của sub class, thành viên protected của base class cũng là thành viên protected của sub class. Các thành viên private của base class không được thừa kế trong sub class.
Kiểu kế thừa Protected: khi một sub class được dẫn xuất từ một protected base class thì cả hai loại thành viên public và protected trong base class sẽ trở thành thành viên protected trong derived class. Các thành viên private của base class không được thừa kế trong sub class.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 94
Kiểu kế thừa Private: khi một sub class được dẫn xuất từ một private base class thì cả hai loại thành viên public và protected trong base class sẽ trở thành thành viên private trong derived class. Các thành viên private của base class không được thừa kế trong sub class.
Quan sát ví dụ sau để hiểu hơn về cơ chế kế thừa: class B {
// Other details omitted public:
void f(); }; void B::f() {
std::cout << "In function 'f'\n"; }
class D: public B {
// Other details omitted public:
void g(); }; void D::g() {
std::cout << "In function 'g'\n"; }
Code phía client: B myB; D myD; myB.f(); myD.f(); myD.g(); Kết quả: In function 'f' In function 'f' In function 'g'
Mặc dù mã nguồn của lớp D không hiển thị rõ ràng định nghĩa của một phương thức có tên f, nó có một phương thức mà nó kế thừa từ lớp B.
Lưu ý rằng kế thừa chỉ hoạt động theo một hướng. Lớp D thừa kế phương thức f từ lớp B, nhưng lớp B không thể kế thừa phương thức d của lớp D. Với các định nghĩa của các lớp B và D ở trên, đoạn mã sau đây là không đúng:
B myB;
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 95
Khơng có giới hạn số lượng lớp kế thừa từ lớp cơ sở:
class D1: public B {/* Details omitted */}; class D2: public B {/* Details omitted */}; class D3: public B {/* Details omitted */}; Lớp D1, D2 và D3 được kế thừa từ lớp B.
Lập trình viên có thể khai báo một derived class có nhiều hơn một base class gọi là đa kế thừa (multiple inheritance) như sau :
class B1 {/* Details omitted */}; class B2 {/* Details omitted */};
class D: public B1, public B2 {/* Details omitted */};
Lớp D có 2 lớp cha là B1 và B2.
Thơng thường các hàm hay phương thức cần object B1 hoặc B2 thì ta có thể thay thế object D. Trong thiết kế hướng đối tượng việc sử dụng đa kế thừa khơng phổ biến vì rất khó kiểm sốt và re-use, đơi khi cũng dùng nhưng là rất hiếm. Ta có thể dùng generic template thay thế cho Multiple Inheritance.
5.4.7| PHÂN LOẠI KẾ THỪA C++ có 5 loại kế thừa sau: C++ có 5 loại kế thừa sau:
• Đơn kế thừa (Single Inheritance): một lớp chỉ kế thừa từ một lớp khác • Đa kế thừa (Multiple Inheritance): Một lớp kế thừa từ nhiều lớp
• Kế thừa nhiều cấp (Multilevel Inheritance): Một lớp vừa là lớp cơ sở của lớp này vừa là lớp dẫn xuất của lớp khác
• Kế thừa Hierarchical: Nhiều lớp kế thừa từ một lớp
• Kế thừa Hybrid: kế thừa thông qua interface (giao diện), virtual inheritance
• Kế thừa multi-path: khi derived class có hai base class và hai base class này có một common base class (Diamond Problem in Inheritance)
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 96 5.4.8| MỘT SỐ MƠ HÌNH KẾ THỪA: Single inheritance 1. //Base Class 2. class A 3. { 4. 5. }; 6. 7. //Derived Class 8. class B : A 9. { 10. 11. }; Multi-level inheritance //Base Class class A { }; //Derived Class class B : A { }; //Derived Class class C : B { } Class A Class B
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 97 Multiple inheritance //Base Class class A { }; class B { }; //Derived Class class C : A, B { }; Hybric inheritance //Base Class class A { }; //Derived Class class B : public A { } ; //Base Class class C { }; //Derived Class
class D : public B, public C { }; Class A Class B Class C Class A Class B Class C Class D
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 98 Hierarchical inheritance class A { }; class B : public A { } ; class C { };
class D : public B, public C { }; Multi-path inheritance class A { }; class B : public A { } ; class C: public A { };
class D : public B, public C {
};
Một số lưu ý:
• Trình biên dịch khơng cho phép sử dụng base class mà đã được khai báo nhưng chưa được định nghĩa.
class X;
class Y: public X { }; // error Class A Class B Class C Class D Class A Class B Class C Class D
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 99
• Derived class kế thừa tồn bộ các thuộc tính non-static của base class. class Base {
public: int a, b; };
class Derived : public Base { public: int c; }; int main() { Derived d; d.a = 1; // Base::a d.b = 2; // Base::b d.c = 3; // Derived::c }
• Trong Derived class có thể khai báo các thành viên có cùng tên với các thành viên của base class. Trong trường hợp trùng tên, ta dùng toán tử :: để phân biệt.
int main() { Derived d;
d.name = "Derived Class"; d.Base::name = "Base Class"; // call Derived::display() d.display(); // call Base::display() d.Base::display(); }
• Ta có thể sử dụng một con trỏ (hoặc tham chiếu) đến một derive class object thay cho một con trỏ hoặc tham chiếu đến base class của nó.
#include <iostream> using namespace std; class Base { public: char* name; void display() {
cout << name << endl; }
};
class Derived: public Base { public:
char* name;
void display() {
cout << name << ", "<< Base::name << endl; }
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 100
};
int main() { Derived d;
d.name = "Derived Class"; d.Base::name = "Base Class";
Derived* dptr = &d;
// Dòng lệnh trên chuyển đổi từ kiểu con trỏ Derived sang kiểu dữ liệu con trỏ của Base.
Base* bptr = dptr;
// call Base::display() bptr->display();
}
• Khơng thể chuyển con trỏ hoặc tham chiếu đến base class sang con trỏ hoặc tham chiếu đến derived class. Ví dụ:
int main() { Base b;
b.name = "Base class";
Derived* dptr = &b; //Error }
5.4.9| DIAMOND PROBLEM: Xét sơ đồ classes sau:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 101
Diamond problem xảy ra trong trường hợp hai supperclass của một class có một base class. Ví dụ: TA class kế thừa từ hai supperclass là Student và Faculty, tuy nhiên hai lớp này có base class là Person. Trường hợp này TA class nhận hai bản sao cho thuộc tính Name và Age. Do đó gây ra sự xung đột dữ liệu.
class Person {
// Data members of person public:
Person(int x) { cout << "Person::Person(int ) called”;}
};
class Faculty : public Person { // data members of Faculty public:
Faculty(int x):Person(x) {
cout<<"Faculty::Faculty(int ) called"<< endl; }
};
class Student : public Person { // data members of Student public:
Student(int x):Person(x) {
cout<<"Student::Student(int ) called"<< endl;} };
class TA : public Faculty, public Student { public:
TA(int x):Student(x), Faculty(x) { cout<<"TA::TA(int ) called"<< endl;} }; int main() { TA ta1(30); } Kết quả: Person::Person(int ) called Faculty::Faculty(int ) called Person::Person(int ) called Student::Student(int ) called TA::TA(int ) called
Hàm tạo và hàm hủy của Person được gọi hai lần nên object ta1 có hai bản sao của Person gây hiện tượng nhập nhằng.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 102
Giải pháp cho vấn đề này là sử dụng từ khóa virtual khi khai báo kế thừa.
5.4.10| VIRTUAL BASE CLASS:
Trường hợp sử dụng một hoặc nhiều objects được dẫn xuất từ một common base class, để tránh hiện tượng tạo nhiều bản sao của base class trong các object của lớp dẫn xuất ta khai báo thêm từ khóa virtual khi khai báo kế thừa. Khi nó base class được hiểu như một virtual base.
Virtual base classes được sử dụng trong kế thừa ảo là một các tránh tạo ra nhiều instance của một class trong mơ hình kế thừa phả hệ khi sử dụng kế thừa multi- path.
Các super class có một virtual base class
Các super class có một virtual base class và một non-virtual base class
class L
{ /* ... */ }; // indirect base class
class B1 : virtual public L
{ /* ... */ };
class B2 : virtual public L { /* ... */ }; class D : public B1, public B2 { /* ... */ }; // valid class V { /* ... */ }; // indirect base class
class B1 : virtual public V { /* ... */ };
class B2 : virtual public V { /* ... */ }; class B3 : public V { /* ... */ }; class X : public B1, public B2 { /* ... */ }; // valid Ví dụ: fancytext.cpp //fancytext.cpp
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 103
#include <string> #include <iostream>
// Base class for all Text derived classes
class Text {
std::string text; public:
// Create a Text object from a client-supplied string
Text(const std::string& t): text(t) {} // Allow clients to see the text field virtual std::string get() const {
return text; }
// Concatenate another string onto the // back of the existing text
virtual void append(const std::string& extra) { text += extra;
} };
// Provides minimal decoration for the text
class FancyText: public Text { std::string left_bracket; std::string right_bracket; std::string connector; public:
// Client supplies the string to wrap plus some extra
// decorations
FancyText(const std::string& t, const std::string& left,
const std::string& right, const std::string& conn):
Text(t), left_bracket(left),
right_bracket(right), connector(conn) {}
// Allow clients to see the decorated text field std::string get() const override {
return left_bracket + Text::get() + right_bracket;
}
// Concatenate another string onto the
// back of the existing text, inserting the connector
// string
void append(const std::string& extra) override { Text::append(connector + extra);
} };
// The text is always the word FIXED
class FixedText: public Text { public:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 104
// wrapped text is always "FIXED" FixedText(): Text("FIXED") {}
// Nothing may be appended to a FixedText object void append(const std::string&) override {
// Disallow concatenation } }; int main() { Text t1("plain"); FancyText t2("fancy", "<<", ">>", "***"); FixedText t3; std::cout << t1.get() << '\n'; std::cout << t2.get() << '\n'; std::cout << t3.get() << '\n'; std::cout << "-------------------------\n"; t1.append("A"); t2.append("A"); t3.append("A"); std::cout << t1.get() << '\n'; std::cout << t2.get() << '\n'; std::cout << t3.get() << '\n'; std::cout << "-------------------------\n"; t1.append("B"); t2.append("B"); t3.append("B"); std::cout << t1.get() << '\n'; std::cout << t2.get() << '\n'; std::cout << t3.get() << '\n'; } Kết quả: plain <<fancy>> FIXED ------------------------- plainA <<fancy***A>> FIXED ------------------------- plainAB <<fancy***A***B>> FIXED
Text làm lớp cơ sở cho hai lớp FanyText và FixedText, một đối tượng Text bao bọc một đối tượng std::string. Vì đối tượng string là private với lớp Text nên người dùng không thể truy cập trực tiếp đối tượng string. Người dùng có thể thấy chuỗi thơng qua phương thức get và chỉ có thể chỉnh sửa một cách hạn chế thông qua phương thức append. Trong ví dụ trên tồn tại hai từ khóa sau:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 105
• Virtual: từ khóa xuất hiện trước phương thức get và append trong lớp
Text mục đích để các derived class có thể tùy biến các hành vi trong các phương thức get và append.
• Override: từ khóa xuất hiện ở cuối định nghĩa phương thức get và append
của lớp FancyText và phương thức append của FixedText, điều này xác định là các hành vi của các phương thức này sẽ khác với lớp Text mà chúng kế thừa. FixedText chỉ gi đè (override) phương thức append, kế thừa hoàn toàn phương thức get.
Phương thức FancyText::get:
class FancyText: public Text { // . . .
public:
// . . .
string get() const override {
return left_bracket + Text::get() + right_bracket;
} };
Lớp FancyText làm thay đổi get. Ta nói lớp FancyText override (ghi đè) phương thức get được nó kế thừa. Từ khóa override nhấn mạnh là thực sự đoạn code trong phương thức get trong FancyText hoạt động khác với get trong Text. Trong trường hợp này, phương thức FancyText::get tạo ra chuỗi từ việc nối ba chuỗi: chuỗi đầu tiên ở phía trước, chuỗi thứ hai ở giữa và chuỗi thứ ba ở phía sau. Lưu ý là chuỗi thứ hai lấy được từ biểu thức Text::get().Câu lệnh trên gọi phương thức get của base class Text. Lưu ý rằng câu lệnh sau khơng hợp lệ vì text là lớp riêng cơ sở (private base class).
return left_bracket + text + right_bracket; // Không hợp lệ
để câu lệnh thực hiện ta phải chỉ rõ Text::get() chứ không chỉ đơn giản là get(). Phương thức get() có nghĩa là this→get, nó sẽ goị FancyText::get. Điều này có nghĩa là FancyText::get được gọi nhầm theo cách không mong muốn.