5 .3| Cerr và clog
5.4 .7| Phân loại kế thừa
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.
Một phương thức được khai báo là virtual trong base class thì sẽ tự động virtual trong devired class. Ta có thể khai báo lại virtual trong derived class nhưng với override thì là dư thừa.
Hàm tạo của lớp Text yêu cầu tham số là chuỗi đơn. Hàm tạo của FancyText địi hỏi phải có 4 tham số truyền vào:
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 106
class FancyText: public Text { // . . .
public:
FancyText(const string& t, const string& left,
const string& right, const string& conn): Text(t), left_bracket(left),
right_bracket(right), connector(conn) {} // . . .
};
Ta muốn gán hàm tạo cho đối số thứ nhất là t, để kế thừa hàm thành viên text, nhưng text là private trong base class. Nghĩa là hàm tạo của FancyText không thể khởi tạo một các trực tiếp, biểu thức đầu tiên trong danh sách các hàm khởi tạo (trong cặp {…}) là Text(t). Đây gọi là hàm tạo lớp cơ sở và truyền t cho nó. Ba biểu thức tiếp theo được khởi tạo theo cách: left_bracket(left), right_bracket(right), connector(conn). Thân hàm tạo là rỗng khi khơng có yêu cầu khởi tạo nào khác.
Lớp FixedText đơn giản hơn lớp FancyText. Nó khơng có field nào và cũng không ghi đè phương thức get. Hàm tạo của nó chấp nhận khơng có đối số bởi vì hàm tạo của nó khơng cho phép người dùng thay đổi nội dung của nó.
Với Lớp Text và FancyText được định nghĩa như hình trên thì đoạn client code sau đây hợp lệ:
Text t1("ABC");
FancyText t2("XYZ", "[", "]", ":");
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 107
t1 = t2;
std::cout << t1.get() << " " << t2.get() << '\n';
Lưu ý là lệnh gán:
t2 = t1; //là không hợp lệ
Tương tự, xét đoạn code sau là hợp lệ:
Text t1("ABC"); FixedText t3;
std::cout << t1.get() << " " << t3.get() << '\n'; t1 = t3;
std::cout << t1.get() << " " << t3.get() << '\n';
t3 =t1; //không hợp lệ
Xem lại đoạn code mơ tả tính kế thừa trong ví dụ sau:
Với đoạn client code sau: Text t1("ABC");
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 108
std::cout << t1.get() << " " << t2.get() << '\n';
Trình biên dịch sẽ thực thi như thế nào với đoạn code t1.get()?
Biến t1 được khai báo kiểu Text nên trình biên dịch sẽ đến Text::get. Khi chương trình gọi t2.get(), chương trình sẽ truyền địa chỉ của đối tượng t2 vào.
Khi trình biên dịch có thể xác định phương thức nào được thực thi dựa vào câu lệnh khai báo đối tượng thì quá trình nhận biết gọi là static binding hay early binding. Static binding được sử dụng trong các trường hợp non-virtual methods và trong trường hợp này nó cũng được sử dụng cho virtual methods.
Trong trường hợp này, nếu ta sử dụng con trỏ đến đối tượng thay cho sử dụng trực tiếp đối tượng thì code như sau:
Text t1("ABC");
FancyText t2("XYZ", "[", "]", ":");
std::cout << t1.get() << " " << t2.get() << '\n'; Text *p1, *p2;
p1 = &t1; p2 = &t2;
P1 vaf p2 là hai con trỏ trỏ đến đối tượng t1 và t2.
p2 là con trỏ đặc biệt trỏ đến đối tượng Text. Đoạn code sau: Text t1("ABC");
FancyText t2("XYZ", "[", "]", ":");
std::cout << t1.get() << " " << t2.get() << '\n'; Text *p1, *p2;
p1 = &t1; p2 = &t2;
std::cout << p1->get() << " " << p2->get() << '\n'; Sẽ cho kết quả:
ABC <<XYZ>> ABC <<XYZ>>
Mặc dù p2 được khai báo là con trỏ trỏ đến Text chứ không phải FancyText, biểu thức p2→get() gọi FancyText::get chứ không gọi Text::get(). Bằng cách nào mà Trình biên dịch xác định phương thức nào được gọi?
Trong trường hợp này virtual method liên chính con trỏ trong chương trình đang chạy quyết định đoạn code nào được thực thi chứ khơng phải trình biên dịch.
Tài liệu giảng dạy Kỹ Thuật Lập Trình 2 Trang 109
Quá trình này được gọi là dynamic binding hay late binding. Static binding theo một cách dễ hiểu là phương thức được thực thi phụ thuộc vào kiểu biến nằm trong phần khai báo hàm. Trình biên dịch dựa vào câu lệnh khai báo kiểu dữ liệu của từng biến vì vậy dễ dàng lựa chọn. Kế thừa là một mối quan hệ phức tạp. Trong ví dụ trên p2 được định nghĩa là một con trỏ trỏ đến Text, nếu trình biên dịch đơn thuần dựa vào lời gọi p2→get() thì Text::get sẽ được gọi. Nhưng p2 thực sự là một con trỏ trỏ đến FancyText, trình biên dịch sẽ thực hiện thế nào? Việc sử dụng con trỏ trong C++ sử dụng static binding cho tất cả các virtual và non-virtual methods thay mặt cho đối tượng. Trình biên dịch có thể chọn phương thức dựa vào câu lệnh khai báo đối tượng. Ngược lại, C++ sử dụng dynamic binding cho lời gọi các virtual methods thông qua con trỏ đến đối tượng. Kiểu đối tượng thực sự sẽ được chọn.