Giao diện
Bài lab · Tuần 6
Tuần 6 — Tính kế thừa
Tuần này thêm vào hệ thống thư viện:
Documentsinh raBookvàMagazine;Readersinh raStudentReaderrồiGradReader(kế thừa ba tầng).
Mang gì từ tuần trước
Chép <MSSV>_w05/ thành <MSSV>_w06/. Lớp Library giữ nguyên, không sửa dòng nào — nhưng nó vẫn phải biên dịch được. Đó là một phần của bài.
Mục tiêu
- Phân biệt
privatevàprotected. - Gọi constructor lớp cha từ constructor lớp con bằng danh sách khởi tạo.
- Dựng chuỗi kế thừa ba tầng và hiểu thứ tự constructor chạy.
- Nhìn thấy tận mắt giới hạn của kế thừa khi chưa có
virtual— đây là cửa vào tuần 7.
Khung bài làm
Tải về, giải nén rồi mở thư mục bằng công cụ bạn dùng. Trong đó có sẵn các file cần nộp với những chỗ ghi TODO để bạn tự viết, cùng file HUONG_DAN.txt ghi cách biên dịch.
Lý thuyết cần nắm
Tuần này Document sinh ra Book và Magazine, còn Reader sinh ra một chuỗi ba tầng. Ví dụ bên dưới vẫn ở chuyện âm nhạc: Track (bản ghi âm) sinh ra Song và Podcast, rồi chuỗi tài khoản nghe nhạc Listener → PremiumListener → FamilyListener. Mọi đoạn code đều đã biên dịch và chạy thật.
1. Kế thừa: lớp con "là một" lớp cha
Bài hát là một bản ghi âm, podcast cũng là một bản ghi âm. Phần chung (tựa, thời lượng, số phút) đặt ở lớp cha Track; mỗi lớp con chỉ thêm phần riêng của mình. Trên sơ đồ, mũi tên có đầu tam giác rỗng chỉ từ lớp con lên lớp cha:


| Thấy trên UML | Viết trong C++ |
|---|---|
Mũi tên tam giác rỗng từ Song lên Track | class Song : public Track { ... }; |
Dấu # ở đầu dòng | đặt dòng đó dưới protected: (mục 2) |
Hộp Song không có title, seconds, minutes() | lớp con tự có sẵn từ lớp cha, không khai báo lại |
Hộp Song có lại dòng + show() : void | lớp con viết lại hàm đó theo cách riêng |
Cho gọn, cả ba lớp nằm chung một file (mục 5 chia file):
cpp
#include <iostream>
#include <string>
using namespace std;
class Track {
protected:
string title;
int seconds;
public:
Track(const string& title, int seconds) : title(title), seconds(seconds) {}
int minutes() const { return seconds / 60; }
void show() const {
cout << "[BAN GHI] " << title << " | " << seconds << " giay" << endl;
}
};
class Song : public Track {
private:
string artist;
public:
Song(const string& title, int seconds, const string& artist)
: Track(title, seconds), artist(artist) {}
void show() const {
cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
}
};
class Podcast : public Track {
private:
string host;
int episode;
public:
Podcast(const string& title, int seconds, const string& host, int episode)
: Track(title, seconds), host(host), episode(episode) {}
void show() const {
cout << "[PODCAST] " << title << " | Tap " << episode << " | " << host << " | " << seconds << " giay" << endl;
}
};
int main() {
Track t("Ban thu am", 95);
Song s("Hello Viet Nam", 263, "Pham Quynh Anh");
Podcast p("Chuyen lap trinh", 1800, "Minh Tu", 12);
t.show();
s.show();
p.show();
cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl; // minutes() ke thua tu Track
return 0;
}text
[BAN GHI] Ban thu am | 95 giay
[BAI HAT] Hello Viet Nam | Pham Quynh Anh | 263 giay
[PODCAST] Chuyen lap trinh | Tap 12 | Minh Tu | 1800 giay
Bai hat dai khoang 4 phuts.minutes()gọi được dùSongkhông viết hàmminutes: hàm đó kế thừa từTrack.- Ba đối tượng gọi ba bản
show()khác nhau: gọi trên biến kiểu nào thì chạy bản của lớp đó. - Constructor của
SongvàPodcastbắt đầu bằngTrack(title, seconds)(mục 3).
2. protected: lớp con dùng được, bên ngoài vẫn không
show() của Song đọc thẳng title và seconds. Nếu hai thuộc tính này là private như tuần 5, g++ chặn luôn cả lớp con (trích):
text
main.cpp: In member function 'void Song::show() const':
main.cpp:29:33: error: 'std::string Track::title' is private within this context
29 | cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
| ^~~~~
main.cpp:7:12: note: declared private here
7 | string title;
| ^~~~~Đổi thành protected thì lớp con dùng được, còn main vẫn bị chặn như cũ:
text
main.cpp: In function 'int main()':
main.cpp:51:15: error: 'std::string Track::title' is protected within this context
51 | cout << s.title << endl;
| ^~~~~
main.cpp:7:12: note: declared protected here
7 | string title;
| ^~~~~| Thuộc tính khai báo là | Trong chính lớp đó | Trong lớp con | Bên ngoài, ví dụ main |
|---|---|---|---|
private | được | không | không |
protected | được | được | không |
public | được | được | được |
Chỉ dùng protected cho thuộc tính mà lớp con thật sự cần đọc. Thuộc tính không lớp con nào cần thì cứ để private, như artist của Song.
3. Constructor lớp con gọi constructor lớp cha
Phần Track bên trong một Song phải do constructor của Track dựng. Vì vậy constructor của lớp con gọi constructor lớp cha ở đầu danh sách khởi tạo, rồi mới tới thuộc tính của riêng nó:
cpp
Song(const string& title, int seconds, const string& artist)
: Track(title, seconds), artist(artist) {}Quên gọi constructor lớp cha. C++ tự gọi constructor mặc định Track(). Track không có constructor đó nên g++ báo (trích):
text
main.cpp: In constructor 'Song::Song(const std::string&, int, const std::string&)':
main.cpp:26:24: error: no matching function for call to 'Track::Track()'
26 | : artist(artist) {}
| ^
main.cpp:26:24: note: there are 3 candidates
main.cpp:11:5: note: candidate 1: 'Track::Track(const std::string&, int)'
11 | Track(const string& title, int seconds) : title(title), seconds(seconds) {}
| ^~~~~
main.cpp:11:5: note: candidate expects 2 arguments, 0 providedNếu lớp cha có constructor mặc định thì không lỗi gì cả: phần lớp cha lặng lẽ nhận giá trị mặc định. Document của bạn đang có constructor mặc định, nên ở Bài 6.1 đây là một lỗi âm thầm.
Tự khởi tạo thuộc tính của lớp cha. Danh sách khởi tạo của Song chỉ nhận thuộc tính khai báo trong chính Song và lớp cha trực tiếp của nó (trích):
text
main.cpp: In constructor 'Song::Song(const std::string&, int, const std::string&)':
main.cpp:26:11: error: class 'Song' does not have any field named 'title'
26 | : title(title), seconds(seconds), artist(artist) {}
| ^~~~~
main.cpp:26:25: error: class 'Song' does not have any field named 'seconds'
26 | : title(title), seconds(seconds), artist(artist) {}
| ^~~~~~~
main.cpp:26:56: error: no matching function for call to 'Track::Track()'Một Song vẫn chứa title, nhưng title thuộc phần Track, nên phải để constructor Track khởi tạo.
4. Nhiều tầng kế thừa và thứ tự constructor
Kế thừa có thể nối nhiều tầng. Tài khoản miễn phí nghe trên 1 thiết bị. Tài khoản Premium là một tài khoản có trả phí, được 3 thiết bị. Gói gia đình là một tài khoản Premium dùng chung, được 6 thiết bị:


Mỗi constructor in một dòng để thấy nó chạy lúc nào:
cpp
#include <iostream>
#include <string>
using namespace std;
class Listener {
protected:
string name;
public:
Listener(const string& name) : name(name) {
cout << " -> constructor Listener" << endl;
}
int maxDevices() const { return 1; }
void show() const {
cout << "[MIEN PHI] " << name << " | " << maxDevices() << " thiet bi" << endl;
}
};
class PremiumListener : public Listener {
protected:
int months; // so thang da tra truoc
public:
PremiumListener(const string& name, int months) : Listener(name), months(months) {
cout << " -> constructor PremiumListener" << endl;
}
int maxDevices() const { return 3; }
void show() const {
cout << "[PREMIUM] " << name << " | Con " << months << " thang | " << maxDevices() << " thiet bi" << endl;
}
};
class FamilyListener : public PremiumListener {
private:
int members;
public:
FamilyListener(const string& name, int months, int members)
: PremiumListener(name, months), members(members) {
cout << " -> constructor FamilyListener" << endl;
}
int maxDevices() const { return 6; }
void show() const {
cout << "[GIA DINH] " << name << " | Con " << months << " thang | " << members << " nguoi | "
<< maxDevices() << " thiet bi" << endl;
}
};
int main() {
cout << "Tao Listener:" << endl;
Listener a("An");
cout << "Tao FamilyListener:" << endl;
FamilyListener f("Nha Binh", 12, 4);
a.show();
f.show();
return 0;
}text
Tao Listener:
-> constructor Listener
Tao FamilyListener:
-> constructor Listener
-> constructor PremiumListener
-> constructor FamilyListener
[MIEN PHI] An | 1 thiet bi
[GIA DINH] Nha Binh | Con 12 thang | 4 nguoi | 6 thiet bi- Tạo một
FamilyListenerlàm chạy cả ba constructor. Đọc output để thấy thứ tự. - Mỗi tầng chỉ gọi lớp cha trực tiếp của mình:
FamilyListenergọiPremiumListener, rồiPremiumListenertự gọiListener. Gọi vượt tầng thì g++ báo (trích):
text
main.cpp: In constructor 'FamilyListener::FamilyListener(const std::string&, int, int)':
main.cpp:43:11: error: type 'Listener' is not a direct base of 'FamilyListener'
43 | : Listener(name), members(members) {
| ^~~~~~~~
main.cpp:43:42: error: no matching function for call to 'PremiumListener::PremiumListener()'show()củaFamilyListenerdùng đượcnamevàmonthsdù chúng khai báo ở hai tầng trên, vì cả hai đềuprotected.
5. Chia file cho lớp con
Mỗi lớp vẫn một cặp .h / .cpp như tuần 4. Điểm mới duy nhất: file .h của lớp con phải #include file .h của lớp cha.
cpp
#ifndef TRACK_H
#define TRACK_H
#include <string>
class Track {
protected:
std::string title;
int seconds;
public:
Track(const std::string& title, int seconds);
int minutes() const;
void show() const;
};
#endifcpp
#include "Track.h"
#include <iostream>
using namespace std;
Track::Track(const string& title, int seconds) : title(title), seconds(seconds) {}
int Track::minutes() const { return seconds / 60; }
void Track::show() const {
cout << "[BAN GHI] " << title << " | " << seconds << " giay" << endl;
}cpp
#ifndef SONG_H
#define SONG_H
#include "Track.h" // phai thay tron lop cha truoc dong "class Song : public Track"
class Song : public Track {
private:
std::string artist;
public:
Song(const std::string& title, int seconds, const std::string& artist);
void show() const;
};
#endifcpp
#include "Song.h"
#include <iostream>
using namespace std;
Song::Song(const string& title, int seconds, const string& artist)
: Track(title, seconds), // goi constructor lop cha TRUOC
artist(artist) {}
void Song::show() const {
cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
}cpp
#include "Song.h"
#include <iostream>
using namespace std;
int main() {
Song s("Hello Viet Nam", 263, "Pham Quynh Anh");
s.show();
cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl;
return 0;
}sh
g++ -std=c++17 -Wall Track.cpp Song.cpp main.cpp -o main.exetext
[BAI HAT] Hello Viet Nam | Pham Quynh Anh | 263 giay
Bai hat dai khoang 4 phutmain.cppchỉ includeSong.h;Trackđi theo nhờ dòng#include "Track.h"trongSong.h.- Lệnh
g++vẫn phải cóTrack.cpp, vì thân các hàm củaTracknằm ở đó. - Thư mục nhiều file thì gõ
g++ -std=c++17 -Wall *.cpp -o main.exeđể biên dịch mọi file.cpptrong thư mục, trong PowerShell hay cmd đều được. Chỉ dùng cách này khi thư mục không có file.cppnào thừa.
Quên #include "Track.h" trong Song.h. Lỗi đầu tiên là dòng đáng đọc. Phía sau là cả loạt lỗi kéo theo, có dòng còn gợi ý sai hướng là thêm #include <string> (trích):
text
In file included from Song.cpp:1:
Song.h:5:27: error: expected class-name before '{' token
5 | class Song : public Track {
| ^
Song.h:7:10: error: 'string' in namespace 'std' does not name a type
7 | std::string artist;
| ^~~~~~
Song.h:1:1: note: 'std::string' is defined in header '<string>'; this is probably fixable by adding '#include <string>'Quên chữ public. Viết class Song : Track thì mọi thứ kế thừa từ Track bị giấu với bên ngoài. Trên ví dụ một file ở mục 1:
text
main.cpp: In function 'int main()':
main.cpp:54:47: error: 'int Track::minutes() const' is inaccessible within this context
54 | cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl; // minutes() ke thua tu Track
| ~~~~~~~~~^~
main.cpp:13:9: note: declared here
13 | int minutes() const { return seconds / 60; }
| ^~~~~~~
main.cpp:54:47: error: 'Track' is not an accessible base of 'Song'
54 | cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl; // minutes() ke thua tu Track
| ~~~~~~~~~^~Bài 6.1 — Kế thừa đơn: Book và Magazine (40 phút)
Mục tiêu 40 phút40:00Chưa bắt đầu
Bước 1. Trong Document.h, đổi private: thành protected:. Trả lời được: vì sao phải đổi? private chặn ai?
Bước 2. Viết hai lớp con:


Book(id, title, year, copies, author, pages)Magazine(id, title, year, copies, issueNo, month)
Constructor lớp con bắt buộc gọi constructor lớp cha ở danh sách khởi tạo:
cpp
Book::Book(const string& id, const string& title, int year, int copies,
const string& author, int pages)
: Document(id, title, year, copies), // goi lop cha TRUOC
author(author), pages(pages) {}Mỗi lớp con viết lại display() theo định dạng riêng của mình.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Chép thư mục <MSSV>_w05 thành <MSSV>_w06, tải khung bài làm tuần 6 và chép mọi file trong đó vào thư mục mới. main.cpp của khung thay cho main.cpp cũ. Biên dịch thử bằng lệnh trong HUONG_DAN.txt: chưa in gì nhưng phải qua.
Bước 2. Trong Document.h, đổi private: thành protected:. Muốn tự thấy vì sao phải đổi, làm xong bước 5 rồi tạm đổi lại private: và biên dịch (trích):
text
Book.cpp: In member function 'void Book::display() const':
Book.cpp:13:26: error: 'std::string Document::id' is private within this context
13 | cout << "[SACH] " << id << " | " << title << " | " << author
| ^~
In file included from Book.h:4,
from Book.cpp:1:
Document.h:10:17: note: declared private hereBook không đọc được id, vì private chặn cả lớp con (mục 2). Xem xong nhớ đổi lại protected:.
Bước 3. Viết Book.h theo sơ đồ: #include "Document.h", class Book : public Document, hai thuộc tính private, constructor sáu tham số và display() const. Không khai báo lại id, title, year, copies (mục 1).
Bước 4. Viết constructor trong Book.cpp đúng như mẫu trong đề: Document(id, title, year, copies) đứng đầu danh sách khởi tạo (mục 3).
Bước 5. Viết Book::display() theo dòng [SACH] của kết quả mong đợi. Để ý thứ tự: tác giả đứng trước năm.
Bước 6. Làm Magazine tương tự. Dòng [TAP CHI] in số, tháng, năm nối bằng dấu /, ví dụ So 57/9/2024.
Bước 7. Trong main.cpp, tạo Book b và Magazine m với dữ liệu lấy từ kết quả mong đợi, rồi gọi display(). Biên dịch, so hai dòng đầu.
Nếu dòng [SACH] in ra ??? | Chua dat ten và năm là 0 mà g++ không cảnh báo gì: constructor của Book chưa gọi constructor Document, nên C++ dùng constructor mặc định của Document (mục 3). Một lần chạy thật với lỗi này:
text
--- 6.1 KE THUA DON ---
[SACH] ??? | Chua dat ten | Bjarne Stroustrup | 0 | 350 trang | Con 0 ban
[TAP CHI] TC001 | Vat ly Ngay nay | So 57/9/2024 | Con 4 banNếu gặp class 'Book' does not have any field named 'id': bạn khởi tạo thuộc tính của Document ngay trong danh sách của Book. Thay bằng một lời gọi Document(id, title, year, copies).
Nếu lỗi đầu tiên là expected class-name before '{' token ở dòng class Book : public Document: Book.h thiếu #include "Document.h". Đừng làm theo gợi ý thêm #include <string> ở các dòng lỗi phía sau (mục 5).
Bài 6.2 — Kế thừa ba tầng: bạn đọc (40 phút)
Mục tiêu 40 phút40:00Chưa bắt đầu


Bước 1 — sửa Reader của tuần 5. Ba thuộc tính id, fullName, borrowing đang để public: đổi thành protected y như đã làm với Document ở bài 6.1. Thêm hai hàm:
std::string getFullName() const— bên ngoài vẫn đọc được tên dù thuộc tính đã đóng (tuần 8 dùng lại hàm này).int maxBorrow() const— bạn đọc thường: 3 tài liệu.
Library không đụng tới Reader nên đổi xong vẫn biên dịch bình thường.
Bước 2 — viết hai lớp con:
Reader(id, fullName)— hạn mức 3 tài liệu.StudentReader(id, fullName, studentId, faculty)— hạn mức 5.GradReader(id, fullName, studentId, faculty, researchField)— hạn mức 10.
Mỗi lớp có maxBorrow() trả hạn mức riêng và display() riêng — định dạng in xem ở mục Kết quả mong đợi.
Câu hỏi phải trả lời: khi tạo một GradReader, constructor nào chạy trước — của GradReader, StudentReader hay Reader? Chèn cout vào từng constructor để tự kiểm chứng, rồi xóa đi trước khi nộp.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Sửa Reader.h theo sơ đồ: thuộc tính chuyển từ public sang protected, thêm maxBorrow() const. Constructor có tham số giữ nguyên. Constructor mặc định của tuần 4 không có trên sơ đồ; bỏ hay giữ đều biên dịch được.
Bước 2. Trong Reader.cpp: maxBorrow() trả về 3; display() đổi sang định dạng [BAN DOC], in hạn mức bằng cách gọi maxBorrow().
Bước 3. Viết StudentReader kế thừa Reader. Constructor gọi Reader(id, fullName) trước. Hai thuộc tính studentId, faculty để protected, vì GradReader ở tầng dưới cần in chúng. maxBorrow() trả 5, display() theo dòng [SINH VIEN].
Bước 4. Viết GradReader kế thừa StudentReader. Constructor năm tham số gọi StudentReader(id, fullName, studentId, faculty), không gọi thẳng Reader (mục 4). maxBorrow() trả 10, display() theo dòng [CAO HOC].
Bước 5. Trong main.cpp, tạo ba bạn đọc theo kết quả mong đợi và gọi display(). Biên dịch, so ba dòng của phần 6.2.
Bước 6. Trả lời câu hỏi của đề bằng thực nghiệm: tạm thêm một dòng cout vào thân constructor của cả ba lớp, ví dụ trong Reader.cpp:
cpp
Reader::Reader(const string& id, const string& fullName)
: id(id), fullName(fullName), borrowing(0) {
cout << " (constructor Reader)" << endl;
}Chạy lại, xem việc tạo GradReader in ra những dòng nào, theo thứ tự nào. Ghi lại, rồi xoá cả ba dòng cout sau khi thử xong.
Nếu gặp type 'Reader' is not a direct base of 'GradReader': constructor của GradReader đang gọi vượt tầng (bước 4). Trích:
text
GradReader.cpp: In constructor 'GradReader::GradReader(const std::string&, const std::string&, const std::string&, const std::string&, const std::string&)':
GradReader.cpp:8:7: error: type 'Reader' is not a direct base of 'GradReader'
8 | : Reader(id, fullName), researchField(researchField) {}
| ^~~~~~Nếu gặp 'std::string StudentReader::studentId' is private within this context trong GradReader.cpp: thuộc tính của StudentReader còn private (bước 3).
Nếu dòng của sinh viên in ra [BAN DOC] ... Han muc 3 tai lieu: StudentReader chưa có display() riêng, nên s.display() chạy bản kế thừa từ Reader, và bản đó gọi maxBorrow() của Reader:
text
[BAN DOC] BD001 | Nguyen Van An | Han muc 3 tai lieu
[BAN DOC] BD002 | Tran Thi Binh | Han muc 3 tai lieuBài 6.3 — Quan sát: gọi qua con trỏ lớp cha (20 phút)
Mục tiêu 20 phút20:00Chưa bắt đầu
Thêm vào cuối main.cpp dòng tiêu đề --- 6.3 ... --- rồi đoạn sau. Nếu bài 6.1 đã tạo sẵn Book b với đúng dữ liệu này thì bỏ dòng đầu, dùng luôn biến đó: khai báo b lần nữa sẽ bị g++ báo redeclaration of 'Book b'.
cpp
Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
Document* p = &b; // con tro Document tro toi mot Book
p->display(); // goi ham NAO?Chạy và ghi lại kết quả. Bạn sẽ thấy nó in ra dòng [TL] ... — tức là bản của lớp cha, dù đối tượng thật sự là một Book.
Viết vào file nhan_xet.txt (3–5 dòng):
- Bạn chờ đợi kết quả nào, và thực tế in ra gì?
- Vì sao trình biên dịch lại chọn hàm của lớp cha?
- Điều này gây rắc rối gì cho
Library::listAll()— vốn duyệt một mảngDocument*?
Tuần 7 sẽ sửa chỗ này bằng đúng một từ khóa.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Ở cuối main, in dòng tiêu đề --- 6.3 QUAN SAT: goi qua con tro lop cha ---, cho con trỏ Document* p trỏ tới Book b đã tạo ở bài 6.1, rồi gọi p->display(). Chép nguyên đoạn trong đề mà main đã có sẵn b thì g++ báo:
text
main.cpp: In function 'int main()':
main.cpp:25:10: error: redeclaration of 'Book b'
25 | Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
| ^
main.cpp:11:10: note: 'Book b' previously declared here
11 | Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
| ^Khi đó bỏ dòng tạo b thứ hai.
Bước 2. Chạy, so với kết quả mong đợi. Dòng cuối là [TL] ..., đúng như đề đã báo trước.
Bước 3. Trước khi viết nhan_xet.txt, làm thêm một thí nghiệm nhỏ. Ngay sau p->display();, thêm:
cpp
Book* q = &b; // cung doi tuong b, nhung con tro kieu Book*
q->display();p và q trỏ tới cùng một đối tượng. Chạy lại và so hai dòng cuối:
text
[TL] S001 | Lap trinh huong doi tuong | 2019 | Con 2 ban
[SACH] S001 | Lap trinh huong doi tuong | Bjarne Stroustrup | 2019 | 350 trang | Con 2 banBước 4. Viết nhan_xet.txt. Mấy câu hỏi dưới đây giúp bạn tự tìm câu trả lời, không phải đáp án:
- Hai lời gọi ở bước 3 khác nhau ở chỗ nào: ở đối tượng, hay ở kiểu của con trỏ?
- Ở dòng
p->display();, trình biên dịch biết chắc điều gì vềp, và điều gì nó không biết được? Library::listAll()gọiitems[i]->display().items[i]có kiểu gì? Nếu thư viện chứa toànBookvàMagazinethì mỗi dòng in ra sẽ bắt đầu bằng gì?
Xoá hai dòng thí nghiệm sau khi thử xong: main.cpp của bài phải in đúng chín dòng của kết quả mong đợi.
Kết quả mong đợi
text
--- 6.1 KE THUA DON ---
[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
--- 6.2 KE THUA DA CAP (3 tang constructor) ---
[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
--- 6.3 QUAN SAT: goi qua con tro lop cha ---
[TL] S001 | Lap trinh huong doi tuong | 2019 | Con 2 banDòng cuối đúng là như vậy — nó không phải lỗi của bạn. Đó là nội dung bài 6.3.
Thư mục bài làm — giữ trên máy, không nộp
text
<MSSV>_w06/
Document.h/.cpp Book.h/.cpp Magazine.h/.cpp
Reader.h/.cpp StudentReader.h/.cpp GradReader.h/.cpp
Library.h/.cpp (giữ nguyên tuần 5)
Newspaper.h/.cpp (bài về nhà, xem cuối trang)
main.cpp
nhan_xet.txtBài tại lớp không nộp. Tuần 6 chỉ nộp bài về nhà ở cuối trang. Giữ nguyên thư mục này: tuần 7 làm tiếp trên chính nó.
Thang điểm — tự chấm bài tại lớp
Bảng này không dùng để lấy điểm; bạn tự soi bài mình trước khi xem đáp án.
| Nội dung | Điểm |
|---|---|
| Chương trình chạy đúng | 2 |
Book, Magazine kế thừa đúng, gọi constructor cha ở danh sách khởi tạo | 2 |
Chuỗi ba tầng Reader → StudentReader → GradReader đúng | 2 |
Dùng protected đúng chỗ, giải thích được | 1 |
nhan_xet.txt của bài 6.3 trả lời đúng trọng tâm | 2 |
| Code tuần 5 vẫn biên dịch được | 1 |
Bài về nhà — chấm điểm
Thêm lớp Newspaper (báo ngày) kế thừa Document. Chỉ một lớp, nhưng nó kiểm tra đúng ba thứ của tuần này: gọi constructor lớp cha, dùng lại hàm của lớp cha, và viết display() riêng cho lớp con.
Đo xem bạn mất bao nhiêu phút và ghi con số đó vào dòng chú thích đầu Newspaper.h. Tuần 9 sẽ thêm một lớp nữa và ta so lại con số đó.
Yêu cầu
Document giữ nguyên bản tuần 6 của bạn — tức là bốn thuộc tính đã ở protected, không sửa thêm gì.
Lớp Newspaper thêm đúng một thuộc tính publishDay kiểu std::string, dạng dd/mm/yyyy (ví dụ "16/10/2026"). Máy chấm so output từng ký tự nên ba chữ ký dưới đây là hợp đồng, giữ nguyên y nguyên:
cpp
class Newspaper : public Document {
private:
std::string publishDay;
public:
Newspaper(const std::string& id, const std::string& title, int year, int copies,
const std::string& publishDay);
std::string getPublishDay() const;
void display() const;
};Constructor bắt buộc gọi constructor
Documentở danh sách khởi tạo, đúng nhưBookở bài 6.1:: Document(id, title, year, copies), publishDay(publishDay) {}. Quên gọi lớp cha thì bốn thuộc tính kia nhận giá trị mặc định (???,Chua dat ten,0,0) và rớt cả bốn bộ kiểm thử.display() constin đúng một dòng, theo định dạng:text[BAO] <id> | <title> | Ra ngay <publishDay> | Con <copies> ban
Chữ
constở cuối không được thiếu. Máy chấm in tờ báo qua tham chiếu hằngconst Newspaper&, nêndisplay(),getPublishDay()và các getter củaDocumentđều phải cóconstở cuối. Thiếu một chữ là không biên dịch được, 0 điểm — đúng cái bẫy đã gặp ở bài về nhà tuần 4, lần này tránh được rồi.
Máy chấm gọi đúng những hàm này
cpp
Newspaper* t = new Newspaper("BND001", "Tuoi Tre", 2026, 5, "16/10/2026");
inBao(*t); // -> display() const, qua tham chieu const
t->getId(); t->getTitle(); t->getYear(); t->getCopies(); // getter cua lop cha
t->getPublishDay();
t->borrow(); t->giveBack(); t->available(); // ham cua lop cha, dung lai duoc
t->setYear(2030); // setter cua lop cha van phai chan nam saiKhông nộp main.cpp: máy đã có main riêng để chạy các kịch bản này.
Kết quả mong đợi của bài về nhà
Một trong bốn bộ kiểm thử, để bạn tự so trước khi nộp:
text
--- TAO BAO ---
[BAO] BND001 | Tuoi Tre | Ra ngay 16/10/2026 | Con 5 ban
[BAO] BND002 | Thanh Nien | Ra ngay 15/10/2026 | Con 3 ban
[BAO] BND003 | Khoa hoc Pho thong | Ra ngay 10/10/2026 | Con 2 ban
--- NGAY RA BAO ---
Ngay ra bao: 16/10/2026
Ngay ra bao: 10/10/2026Chấm điểm
Mười điểm chia đều cho bốn bộ kiểm thử, mỗi bộ 2,5 điểm.
| Bộ kiểm thử | Điểm |
|---|---|
| Tạo ba tờ báo, in ra, đọc ngày ra báo | 2,5 |
| Getter và mượn/trả của lớp cha chạy trên lớp con | 2,5 |
setYear vẫn chặn năm sai khi gọi trên Newspaper | 2,5 |
| Nhiều tờ báo: tờ hết bản, tờ còn bản, mã không có | 2,5 |
Nộp sau hạn thì trừ 10% điểm bài đó — được 10 điểm mà nộp trễ thì còn 9. Quá hạn vẫn nộp và vẫn bấm Đánh giá được, nên trễ thì cứ nộp, đừng bỏ bài.
Nộp bài
- Nộp đúng bốn tệp:
Document.h,Document.cpp,Newspaper.h,Newspaper.cpp. - Không nộp
main.cpp, cũng không nộpBook.*,Magazine.*,Reader.*,Library.*. - Trong hoạt động Phòng lập trình ảo trên Moodle, bốn tệp mở sẵn chỉ có mấy dòng
// TODO: mở file trên máy bạn, chép hết, dán đè vào từng tệp — đừng gõ lại từ đầu trong trình duyệt. - Dán xong bấm Lưu, rồi bấm Đánh giá để thấy điểm và biết bộ nào sai. Nộp lại không giới hạn số lần, điểm lấy lần cuối — nhưng bấm Lưu thôi thì chưa có điểm.
- Hạn nộp 19:00, trước buổi lab tuần 7 của lớp bạn: lớp học thứ hai — 19:00 chủ nhật 25/10; lớp học thứ tư — 19:00 thứ ba 27/10. Ngày giờ chính xác luôn hiện ngay trong hoạt động trên Moodle.
Đáp án tuần này
Nộp bài xong rồi mở trang đáp án tuần 6 và gõ mã mở đáp án giảng viên đăng trên Moodle. Mã đó chỉ hiện ra sau khi bạn đã nộp bài: đọc đáp án trước khi tự làm thì buổi lab thành vô ích.