Giao diện
Chương 4
Tính kế thừa
Chương này ăn điểm ở đâu trong đề thi
- Thiết kế đúng lớp trừu tượng, kế thừa đầy đủ2 điểm, có ở 12/12 đề
- Cài đặt đúng các lớp con, có thuộc tính riêng2 điểm
- Hàm khởi tạo đúng (chuỗi constructor)1 điểm, có ở 4/12 đề
4.1 Vấn đề
Thư viện không chỉ có tài liệu chung chung. Nó có sách (có tác giả, có số trang), tạp chí (có số kỳ, có tháng), đĩa (có thời lượng). Cách ngây thơ là viết ba lớp riêng:
cpp
class Book { string id, title; int year, copies; string author; int pages; };
class Magazine { string id, title; int year, copies; int issueNo, month; };
class Disc { string id, title; int year, copies; int minutes; };Bốn thuộc tính đầu lặp ba lần. Thêm một thuộc tính chung — chẳng hạn “kệ số mấy” — là phải sửa ba chỗ. Viết hàm display() thì viết ba lần gần như y hệt. Và quan trọng nhất: không có cách nào bỏ chung ba thứ đó vào một danh sách.
Kế thừa gom phần chung lên một lớp cha, phần riêng để lại lớp con.


Câu thử để biết có nên kế thừa không
Đọc thành câu: “Book là một Document”. Đúng → kế thừa hợp lý.
“Library là một Document” → sai. Thư viện chứa tài liệu, không là tài liệu. Quan hệ chứa thì dùng thuộc tính, không dùng kế thừa.
Nhầm hai quan hệ này là lỗi thiết kế nặng nhất trong đề thi, và mất trọn 2 điểm “thiết kế đúng lớp”.
4.2 private hay protected
Chương 3 để thuộc tính private. Bây giờ Book::display() cần in title — mà private thì lớp con cũng không chạm được. Đó là lý do có mức thứ ba:
cpp
class Document {
protected: // lop con dung duoc, ben ngoai van khong
std::string id;
std::string title;
int year;
int copies;
public:
// ... getter, setter, borrow, giveBack nhu chuong 3
};protected không phải là cái cớ để mở hết
Chỉ chuyển sang protected những thuộc tính lớp con thật sự cần. Phần còn lại để private và cho lớp con dùng getter — lớp con gọi được getter public bình thường.
Đề thi ghi “private/protected + getter/setter”, nghĩa là cả hai đều được chấp nhận. Cái bị trừ điểm là public.
4.3 Viết một lớp con
cpp
#ifndef BOOK_H
#define BOOK_H
#include "Document.h"
class Book : public Document { // <- dong quan trong nhat
private:
std::string author;
int pages;
public:
Book(const std::string& id, const std::string& title, int year, int copies,
const std::string& author, int pages);
std::string getAuthor() const;
void display() const;
};
#endifclass Book : public Document— dấu hai chấm rồipublicrồi tên lớp cha. Quên chữpublicthì mặc định thànhprivatevà mọi thứ kế thừa được sẽ bị khoá lại; đây là lỗi im lặng rất khó tìm.- Không khai báo lại
id,title… Lớp con đã có chúng rồi. Khai báo lại là tạo ra bản thứ hai che mất bản gốc. #include "Document.h"là bắt buộc — muốn kế thừa ai thì phải biết người đó trước.
4.4 Chuỗi constructor — phần hay sai nhất
Khi tạo một Book, phần Document bên trong nó phải được khởi tạo trước. C++ bắt bạn nói rõ khởi tạo bằng cách nào, và chỗ duy nhất nói được là danh sách khởi tạo:
text
Book::Book(const string& id, const string& title, int year, int copies,
const string& author, int pages)
: Document(id, title, year, copies), // goi constructor lop CHA truoc
author(author), pages(pages) {} // roi den phan rieng cua minhThứ tự chạy — một câu hỏi hay bị hỏi
Constructor lớp cha chạy trước, constructor lớp con chạy sau. Destructor thì ngược lại: lớp con huỷ trước, lớp cha huỷ sau.
Dễ nhớ: xây nhà thì làm móng trước; đập nhà thì dỡ mái trước.
Tự kiểm chứng bằng cách chèn cout vào từng constructor, chạy, rồi xoá đi.
Lỗi thường gặp
cpp
// SAI - gan trong than ham
Book::Book(const string& id, ...) {
this->id = id; // error: 'id' is a protected member... hoac
} // lop cha da bi khoi tao bang constructor mac dinh roi
// SAI - quen goi constructor cha khi cha KHONG co constructor mac dinh
Book::Book(...) : author(author), pages(pages) {}
// error: no matching function for call to 'Document::Document()'Thông báo no matching function for call to 'Document::Document()' trong một constructor lớp con hầu như luôn có nghĩa: bạn quên gọi constructor lớp cha.
4.5 Kế thừa nhiều tầng
Lớp con có thể lại làm cha cho lớp khác. Phía bạn đọc của thư viện có ba tầng:


Mỗi tầng chỉ cần biết tầng ngay trên nó:
text
StudentReader::StudentReader(const string& id, const string& fullName,
const string& studentId, const string& faculty)
: Reader(id, fullName), studentId(studentId), faculty(faculty) {}
GradReader::GradReader(const string& id, const string& fullName,
const string& studentId, const string& faculty,
const string& researchField)
: StudentReader(id, fullName, studentId, faculty),
researchField(researchField) {}GradReader không gọi thẳng Reader(...) — nó gọi StudentReader(...), và StudentReader tự lo gọi Reader. Mỗi tầng chỉ chịu trách nhiệm với tầng kề nó.
4.6 Một quan sát sẽ được giải thích ở chương 5
Viết lại một hàm của lớp cha trong lớp con — như Book::display() vừa rồi — bài giảng tuần 6 gọi là redefining (định nghĩa lại). Nó không giống nạp chồng (overloading): nạp chồng đòi danh sách tham số phải khác, còn redefining thì giữ nguyên tên và tham số, chỉ đổi phần thân.
Bài giảng tuần 6 có hẳn một slide tên “Problem with Redefining”. Đoạn code dưới đây chính là vấn đề đó, và bạn nên tự chạy nó trước khi đọc tiếp:
cpp
Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
b.display(); // (1)
Document* p = &b; // con tro Document tro toi mot Book
p->display(); // (2) goi ham NAO?Kết quả thật:
text
[SACH] S001 | Lap trinh huong doi tuong | Bjarne Stroustrup | 2019 | 350 trang | Con 2 ban
[TL] S001 | Lap trinh huong doi tuong | 2019 | Con 2 banĐọc kỹ hai dòng trên
Cùng một đối tượng, cùng tên hàm, hai kết quả khác nhau. Dòng (2) chạy bản của lớp cha, dù thứ nó đang trỏ tới rõ ràng là một Book.
Vì sao lại tệ? Vì Library của bạn giữ một mảng Document*. Nghĩa là listAll() sẽ in mọi cuốn sách dưới dạng tài liệu chung — mất sạch tác giả và số trang.
Chương 5 sửa chỗ này bằng đúng một từ khoá. Trước khi sang đó, hãy tự chạy thử đoạn trên và viết ra giấy vì sao bạn nghĩ nó lại chọn hàm của lớp cha.
4.7 Làm theo từng bước: dựng cây kế thừa từ sơ đồ
Sơ đồ dưới đây có ba hộp và hai mũi tên. Theo bảng dịch ở mục 2.5: ba hộp → ba cặp file; mũi tên tam giác rỗng → class Con : public Cha.


Bước 1. Chép thư mục chương trước, đổi private thành protected
sh
xcopy C:\OOP\uml2 C:\OOP\uml3\ /E /I
cd C:\OOP\uml3Trong Document.h, đổi nhãn private: thành protected:. Sơ đồ đã báo trước điều này bằng dấu # thay cho dấu -.
Biên dịch lại ngay để chắc chắn chưa hỏng gì: g++ -std=c++17 -Wall *.cpp -o main.exe.
Bước 2. Tạo Book.h — chỉ khung và dòng kế thừa
cpp
#ifndef BOOK_H
#define BOOK_H
#include "Document.h" // muon ke thua ai thi phai biet nguoi do truoc
class Book : public Document { // <- dich tu mui ten tren so do
};
#endifsh
g++ -std=c++17 -fsyntax-only Book.hBạn sẽ thấy:
text
(khong in ra gi ca)Bước 3. Dịch hai tầng còn lại của hộp Book
Dấu - → private. Chú ý thứ tự tham số của constructor: thuộc tính của cha trước, của con sau — đó là quy ước của cả sách và cũng là cách đề thi hay viết.
cpp
class Book : public Document {
private:
std::string author;
int pages;
public:
Book(const std::string& id, const std::string& title, int year, int copies,
const std::string& author, int pages);
std::string getAuthor() const;
void display() const;
};Bước 4. Cố ý quên gọi constructor lớp cha để thấy lỗi
Trong Book.cpp, viết constructor nhưng bỏ phần gọi lớp cha:
text
Book::Book(const string& id, const string& title, int year, int copies,
const string& author, int pages)
: author(author), pages(pages) {} // thieu : Document(...)Nếu hiện ra lỗi này:
text
Book.cpp:5:38: error: no matching function for call to 'Document::Document()'
5 | : author(author), pages(pages) {}
| ^
Document.h:12:5: note: candidate: 'Document::Document(const std::string&,
const std::string&, int, int)'
Document.h:12:5: note: candidate expects 4 arguments, 0 providedĐọc: “cần một Document::Document() không tham số nhưng không có”. Vì bạn không nói khởi tạo phần cha thế nào, C++ đi tìm constructor mặc định — và Document không có. Dòng note còn liệt kê constructor nào đang tồn tại.
Bước 5. Sửa lại cho đúng
cpp
Book::Book(const string& id, const string& title, int year, int copies,
const string& author, int pages)
: Document(id, title, year, copies), // cha truoc
author(author), pages(pages) {} // con sau
string Book::getAuthor() const { return author; }
void Book::display() const {
cout << "[SACH] " << id << " | " << title << " | " << author
<< " | " << year << " | " << pages << " trang"
<< " | Con " << copies << " ban" << endl;
}Chú ý: display() của Book dùng thẳng id, title — được, vì bước 1 đã đổi sang protected. Nếu vẫn để private thì dòng này không biên dịch.
Bước 6. Làm tương tự cho Magazine, rồi chạy
sh
g++ -std=c++17 -Wall *.cpp -o main.exe
main.exeBạn sẽ thấy:
text
[SACH] S001 | Lap trinh huong doi tuong | Bjarne Stroustrup | 2019 | 350 trang | Con 2 ban
[TAP CHI] TC001 | Vat ly Ngay nay | So 57/9/2024 | Con 4 banBước 7. Dựng chuỗi ba tầng phía bạn đọc
Cùng một phép dịch, chỉ khác là mũi tên nối tiếp nhau. Làm lại bước 2–5 ba lần cho Reader, StudentReader, GradReader. Mỗi tầng chỉ gọi constructor của tầng ngay trên nó.
Bạn sẽ thấy:
text
[BAN DOC] BD001 | Nguyen Van An | Han muc 3 tai lieu
[SINH VIEN] BD002 | Tran Thi Binh | MSSV 23130007 | Khoa Vat ly | Han muc 5
[CAO HOC] BD003 | Le Minh Cuong | MSSV 22C12345 | Khoa Vat ly | Huong NC: Vat ly chat ran | Han muc 10Bước 8. Đối chiếu ngược lại với sơ đồ
Tích từng dòng: đủ ba hộp chưa, mũi tên đúng chiều chưa, dấu # đã thành protected chưa, mỗi constructor có gọi lớp cha chưa. 30 giây, cứu 2 điểm.
Tự kiểm tra
35 phút, đóng sách.
- Chuyển thuộc tính của
Documentsangprotected. Giải thích trong một câu vì sao phải đổi. - Viết
Book(thêmauthor,pages) vàMagazine(thêmissueNo,month), mỗi lớp códisplay()riêng. - Viết chuỗi ba tầng
Reader → StudentReader → GradReadervới hạn mức 3 / 5 / 10. - Cố ý bỏ phần
: Document(id, title, year, copies)trong constructor củaBook. Ghi lại thông báo lỗi. - Chạy đoạn ở mục 4.6 và ghi lại kết quả cùng lời giải thích của bạn.
Kết quả mong đợi của bài 2 và 3:
text
[SACH] S001 | Lap trinh huong doi tuong | Bjarne Stroustrup | 2019 | 350 trang | Con 2 ban
[TAP CHI] TC001 | Vat ly Ngay nay | So 57/9/2024 | Con 4 ban
[BAN DOC] BD001 | Nguyen Van An | Han muc 3 tai lieu
[SINH VIEN] BD002 | Tran Thi Binh | MSSV 23130007 | Khoa Vat ly | Han muc 5
[CAO HOC] BD003 | Le Minh Cuong | MSSV 22C12345 | Khoa Vat ly | Huong NC: Vat ly chat ran | Han muc 10