Giao diện
Bài lab · Tuần 7
Tuần 7 — Tính đa hình
Tuần này thêm vào hệ thống thư viện: từ khóa
virtualđể sửa đúng vấn đề phát hiện ở bài 6.3, hạn mượn khác nhau theo từng loại tài liệu, và lớpLoanDateđể tính ngày trả — nơi luyện nạp chồng toán tử.
Mang gì từ tuần trước
Chép <MSSV>_w06/ thành <MSSV>_w07/. Mở lại nhan_xet.txt của bài 6.3 trước khi bắt đầu.
Mục tiêu
- Dùng
virtualđể lời gọi qua con trỏ lớp cha chạy đúng hàm của lớp con. - Hiểu vì sao lớp cha có hàm ảo thì bắt buộc phải có destructor ảo.
- Nạp chồng toán tử
+,-,<<.
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 bạn sửa đúng chỗ bài 6.3 đã phát hiện bằng virtual, thêm destructor ảo, và viết lớp LoanDate có ba toán tử. Ví dụ vẫn ở chuyện âm nhạc: Track, Song, Podcast như tuần 6, thêm lớp Duration (thời lượng). Mục cuối là phần chuẩn bị cho bài chấm điểm tại lớp tuần 8. Mọi đoạn code đều đã biên dịch và chạy thật.
Về việc chia
.hvà.cpp. Nhiều bạn phản ánh là trang lý thuyết gom cả lớp vào một đoạn code, trong khi bài thực hành lại bắt tách ra — đúng là lệch, và từ nay trang lý thuyết chia đúng như khi bạn làm bài. Cách chia nằm ở mục 5 của trang tuần 4.
1. virtual: chọn hàm theo đối tượng thật
Tuần 6, gọi qua con trỏ lớp cha luôn chạy bản của lớp cha. Thêm virtual trước hàm ở lớp cha và override ở lớp con thì khác hẳn:


| Thấy trên UML | Viết trong file .h |
|---|---|
+ show() : void {virtual} ở lớp cha | virtual void show() const; |
+ show() : void {override} ở lớp con | void show() const override; |
+ ~Track() {virtual} | virtual ~Track(); (mục 3) |
virtual và override chỉ viết ở file .h. Dòng định nghĩa trong .cpp giữ nguyên như cũ, không lặp lại hai từ này — chép sang là lỗi biên dịch. Để ý điều đó trong Song.cpp và Podcast.cpp dưới đây.
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);
virtual void show() const;
virtual int price() const; // gia tai ve, tinh bang dong
void receipt() const; // KHONG ao, nhung goi mot ham ao
virtual ~Track();
};
#endifcpp
#include "Track.h"
#include <iostream>
using namespace std;
Track::Track(const string& title, int seconds) : title(title), seconds(seconds) {}
void Track::show() const {
cout << "[BAN GHI] " << title << " | " << seconds << " giay" << endl;
}
int Track::price() const { return 5000; }
void Track::receipt() const {
cout << " Tai " << title << ": " << price() << " dong" << endl;
}
Track::~Track() {}cpp
#ifndef SONG_H
#define SONG_H
#include "Track.h"
class Song : public Track {
private:
std::string artist;
public:
Song(const std::string& title, int seconds, const std::string& artist);
void show() const override;
int price() const override;
};
#endifcpp
#include "Song.h"
#include <iostream>
using namespace std;
Song::Song(const string& title, int seconds, const string& artist)
: Track(title, seconds), artist(artist) {}
void Song::show() const {
cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
}
int Song::price() const { return 10000; }cpp
#ifndef PODCAST_H
#define PODCAST_H
#include "Track.h"
class Podcast : public Track {
private:
int episode;
public:
Podcast(const std::string& title, int seconds, int episode);
void show() const override;
int price() const override;
};
#endifcpp
#include "Podcast.h"
#include <iostream>
using namespace std;
Podcast::Podcast(const string& title, int seconds, int episode)
: Track(title, seconds), episode(episode) {}
void Podcast::show() const {
cout << "[PODCAST] " << title << " | Tap " << episode << " | " << seconds << " giay" << endl;
}
int Podcast::price() const { return 0; }cpp
#include "Track.h"
#include "Song.h"
#include "Podcast.h"
int main() {
Track* ds[3];
ds[0] = new Song("Hello Viet Nam", 263, "Pham Quynh Anh");
ds[1] = new Podcast("Chuyen lap trinh", 1800, 12);
ds[2] = new Track("Ban thu am", 95);
for (int i = 0; i < 3; i++)
ds[i]->show(); // cung mot loi goi, ba ket qua
for (int i = 0; i < 3; i++)
ds[i]->receipt();
for (int i = 0; i < 3; i++)
delete ds[i];
return 0;
}text
[BAI HAT] Hello Viet Nam | Pham Quynh Anh | 263 giay
[PODCAST] Chuyen lap trinh | Tap 12 | 1800 giay
[BAN GHI] Ban thu am | 95 giay
Tai Hello Viet Nam: 10000 dong
Tai Chuyen lap trinh: 0 dong
Tai Ban thu am: 5000 dong- Cả ba phần tử của
dsđều có kiểuTrack*, nhưng mỗi dòng in theo đúng đối tượng thật. Hàmvirtualđược chọn lúc chạy, theo đối tượng mà con trỏ đang trỏ tới. receipt()khôngvirtual, nhưng bên trong gọiprice()là hàm ảo, nên giá vẫn đúng theo từng loại.
Bỏ virtual ở lớp cha (và override ở lớp con) thì quay về đúng hiện tượng của bài 6.3:
text
[BAN GHI] Hello Viet Nam | 263 giay
[BAN GHI] Chuyen lap trinh | 1800 giay
[BAN GHI] Ban thu am | 95 giay
Tai Hello Viet Nam: 5000 dong
Tai Chuyen lap trinh: 5000 dong
Tai Ban thu am: 5000 dong2. override: để g++ bắt lỗi gõ sai
Hàm ở lớp con phải trùng chữ ký với hàm ảo của lớp cha, kể cả chữ const. Viết void show() override { mà quên const:
text
In file included from main.cpp:2:
Song.h:13:10: error: 'void Song::show()' marked 'override', but does not override
13 | void show() override;
| ^~~~Quên const mà cũng không có override thì chương trình vẫn biên dịch được. show() của Song lúc này là một hàm khác, không ghi đè gì cả, và g++ chỉ để lại một cảnh báo rất dễ bỏ qua:
text
In file included from main.cpp:1:
Track.h:14:18: warning: 'virtual void Track::show() const' was hidden [-Woverloaded-virtual=]
14 | virtual void show() const;
| ^~~~
In file included from main.cpp:2:
Song.h:13:10: note: by 'void Song::show()'
13 | void show();
| ^~~~text
[BAN GHI] Hello Viet Nam | 263 giay
[PODCAST] Chuyen lap trinh | Tap 12 | 1800 giay
[BAN GHI] Ban thu am | 95 giayBài hát lại in thành [BAN GHI]. Có override, lỗi này hiện ra ngay lúc biên dịch. Cùng thông báo marked 'override', but does not override cũng xuất hiện khi lớp cha quên virtual.
3. Destructor ảo
Lớp con có thể xin thêm bộ nhớ riêng, như Song dưới đây giữ lời bài hát bằng new. Muốn delete qua con trỏ lớp cha mà vẫn dọn được phần đó, destructor của lớp cha phải virtual:
cpp
#ifndef TRACK_H
#define TRACK_H
#include <string>
class Track {
protected:
std::string title;
public:
Track(const std::string& title);
virtual void show() const;
virtual ~Track();
};
#endifcpp
#include "Track.h"
#include <iostream>
using namespace std;
Track::Track(const string& title) : title(title) {}
void Track::show() const {
cout << "[BAN GHI] " << title << endl;
}
Track::~Track() {
cout << " (huy Track " << title << ")" << endl;
}cpp
#ifndef SONG_H
#define SONG_H
#include "Track.h"
class Song : public Track {
private:
std::string* lyrics; // loi bai hat, xin bang new
public:
Song(const std::string& title, const std::string& text);
void show() const override;
~Song() override;
};
#endifcpp
#include "Song.h"
#include <iostream>
using namespace std;
Song::Song(const string& title, const string& text)
: Track(title), lyrics(new string(text)) {}
void Song::show() const {
cout << "[BAI HAT] " << title << " | " << *lyrics << endl;
}
Song::~Song() {
delete lyrics;
cout << " (huy Song " << title << ", da tra loi bai hat)" << endl;
}cpp
#include "Track.h"
#include "Song.h"
int main() {
Track* a = new Song("Hello Viet Nam", "Xin chao Viet Nam...");
Track* b = new Track("Ban thu am");
a->show();
b->show();
delete a;
delete b;
return 0;
}text
[BAI HAT] Hello Viet Nam | Xin chao Viet Nam...
[BAN GHI] Ban thu am
(huy Song Hello Viet Nam, da tra loi bai hat)
(huy Track Hello Viet Nam)
(huy Track Ban thu am)Destructor của lớp con chạy trước, rồi mới tới lớp cha: dọn phần riêng trước, phần chung sau.
Bỏ virtual ở ~Track() mà ~Song() vẫn còn override thì không biên dịch được:
text
In file included from main.cpp:2:
Song.h:14:5: error: 'Song::~Song()' marked 'override', but does not override
14 | ~Song() override;
| ^Bỏ luôn override thì biên dịch được, kèm cảnh báo:
text
main.cpp: In function 'int main()':
main.cpp:10:5: warning: deleting object of polymorphic class type 'Track' which has non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor]
10 | delete a;
| ^~~~~~~~text
[BAI HAT] Hello Viet Nam | Xin chao Viet Nam...
[BAN GHI] Ban thu am
(huy Track Hello Viet Nam)
(huy Track Ban thu am)Destructor của Song không chạy, nên lời bài hát xin bằng new không bao giờ được trả. Luật để nhớ: lớp nào có hàm virtual thì destructor cũng phải virtual.
4. Nạp chồng toán tử
Lớp Duration giữ thời lượng dạng phút và giây. Nạp chồng toán tử cho phép viết a + b, a - b, cout << a như với số:


cpp
#ifndef DURATION_H
#define DURATION_H
#include <iostream>
class Duration {
private:
int minutes, seconds; // luon giu 0 <= seconds < 60
public:
Duration(int m, int s);
Duration operator+(const Duration& other) const;
int operator-(const Duration& other) const; // chenh lech, tinh bang giay
friend std::ostream& operator<<(std::ostream& os, const Duration& d);
};
#endifcpp
#include "Duration.h"
using namespace std;
Duration::Duration(int m, int s) : minutes(m), seconds(s) {
minutes += seconds / 60; // 3 phut 75 giay -> 4 phut 15 giay
seconds %= 60;
}
Duration Duration::operator+(const Duration& other) const {
return Duration(minutes + other.minutes, seconds + other.seconds); // doi tuong MOI
}
int Duration::operator-(const Duration& other) const {
return (minutes * 60 + seconds) - (other.minutes * 60 + other.seconds);
}
// operator<< la ham TU DO (friend), nen khong co Duration:: o truoc
ostream& operator<<(ostream& os, const Duration& d) {
os << d.minutes << ':';
if (d.seconds < 10) os << '0';
os << d.seconds;
return os;
}cpp
#include "Duration.h"
#include <iostream>
using namespace std;
int main() {
Duration a(4, 23), b(3, 45);
Duration c = a + b;
cout << a << " + " << b << " = " << c << endl;
cout << "Bai dau dai hon bai sau " << (a - b) << " giay" << endl;
cout << "Sau phep cong, a van la " << a << endl;
return 0;
}text
4:23 + 3:45 = 8:08
Bai dau dai hon bai sau 38 giay
Sau phep cong, a van la 4:23a + blà cách viết gọn củaa.operator+(b). Hàm cóconstvà trả về một đối tượng mới, nênagiữ nguyên.- Kiểu trả về do bạn chọn:
operator+trảDuration,operator-trảint(số giây chênh lệch). operator<<không làm hàm thành viên củaDurationđược, vì bên trái dấu<<làcoutchứ không phải mộtDuration. Nó là hàm ngoài lớp, được khai báofriendđể đọc thuộc tínhprivate. Nó nhậnostream&và trả lại chínhos, nhờ vậy mới viết nối đượccout << a << " + " << b.- In số 0 ở đầu khi cần:
if (d.seconds < 10) os << '0';.
Quên friend. Khai báo trong lớp khi đó là một hàm thành viên. Hàm thành viên đã ngầm nhận đối tượng hiện tại làm toán hạng bên trái, cộng thêm hai tham số là thừa một. Lỗi hiện ra ở Duration.h, nơi khai báo (trích):
text
In file included from Duration.cpp:1:
Duration.h:16:19: error: 'std::ostream& Duration::operator<<(std::ostream&, const Duration&)' must have exactly one argument
16 | std::ostream& operator<<(std::ostream& os, const Duration& d); // SAI: thieu friend
| ^~~~~~~~Trả về void. cout << a khi đó không trả lại gì để nối tiếp << " + " (trích):
text
main.cpp: In function 'int main()':
main.cpp:8:15: error: invalid operands of types 'void' and 'const char [4]' to binary 'operator<<'
8 | cout << a << " + " << b << " = " << c << endl;
| ~~~~~~~~~ ^~ ~~~~~
| | |
| void const char [4]Sửa luôn đối tượng bên trái. Bỏ const rồi cộng thẳng vào thuộc tính:
cpp
Duration Duration::operator+(const Duration& other) { // SAI: sua chinh doi tuong ben trai
minutes += other.minutes;
seconds += other.seconds;
minutes += seconds / 60;
seconds %= 60;
return *this;
}text
8:08 + 3:45 = 8:08
Bai dau dai hon bai sau 263 giay
Sau phep cong, a van la 8:08Phép cộng làm a đổi thành 8:08, kéo theo cả dòng in và phép trừ phía sau sai. Giữ const ở Duration.h thì g++ chặn ngay lỗi này, và chặn ở Duration.cpp — nơi viết thân hàm (trích):
text
Duration.cpp: In member function 'Duration Duration::operator+(const Duration&) const':
Duration.cpp:10:13: error: assignment of member 'Duration::minutes' in read-only object
10 | minutes += other.minutes; // SAI: ham co const, khong sua duoc thuoc tinh
| ~~~~~~~~^~~~~~~~~~~~~~~~5. Chuẩn bị cho bài kiểm tra thực hành trên lớp
Bài kiểm tra 15% điểm môn — lớp thứ tư 18/11, lớp thứ hai 23/11
Làm trong 60 phút, không dùng AI, không hỏi bạn, không mở bài của bạn khác; được mở giáo trình, slide và thư mục bài làm của chính bạn các tuần trước. Đề là một đề riêng, miền hoàn toàn khác hệ thống thư viện, và không có khung bài làm: bạn tự tạo thư mục <MSSV>_kiemtra cùng từng tệp .h / .cpp, rồi nén thành <MSSV>_kiemtra.zip nộp lên Moodle ngay trong buổi.
Đề chỉ dùng tới hết đa hình: một lớp cha cụ thể, hai lớp con ghi đè, vector<Cha*> trong main, destructor ảo, và một setter có kiểm tra. Chưa tới lớp trừu tượng và ngoại lệ. Nói cách khác: làm trôi chảy mục 1 tới mục 4 của trang này là đủ.
Mục 5 dưới đây đi xa hơn bài kiểm tra một bước — nó có một lớp giữ đối tượng của lớp khác làm thuộc tính. Thi cuối kỳ hay hỏi kiểu đó, nên tự gõ lại ví dụ này là đáng công. Chương trình lập lịch phát cho một danh sách bản ghi: Slot là một khung phát, giờ bắt đầu truyền vào, còn giờ kết thúc tính ra từ bản ghi mà nó giữ.
cpp
#ifndef DURATION_H
#define DURATION_H
#include <iostream>
class Duration {
private:
int minutes, seconds; // luon giu 0 <= seconds < 60
public:
Duration(int m, int s);
Duration operator+(const Duration& other) const;
int operator-(const Duration& other) const; // chenh lech, tinh bang giay
friend std::ostream& operator<<(std::ostream& os, const Duration& d);
};
#endifcpp
#include "Duration.h"
using namespace std;
Duration::Duration(int m, int s) : minutes(m), seconds(s) {
minutes += seconds / 60; // 3 phut 75 giay -> 4 phut 15 giay
seconds %= 60;
}
Duration Duration::operator+(const Duration& other) const {
return Duration(minutes + other.minutes, seconds + other.seconds); // doi tuong MOI
}
int Duration::operator-(const Duration& other) const {
return (minutes * 60 + seconds) - (other.minutes * 60 + other.seconds);
}
// operator<< la ham TU DO (friend), nen khong co Duration:: o truoc
ostream& operator<<(ostream& os, const Duration& d) {
os << d.minutes << ':';
if (d.seconds < 10) os << '0';
os << d.seconds;
return os;
}cpp
#ifndef TRACK_H
#define TRACK_H
#include <string>
#include "Duration.h"
class Track {
protected:
std::string title;
Duration length; // mot doi tuong Duration nam ngay trong Track
public:
Track(const std::string& title, const Duration& length);
virtual ~Track();
std::string getTitle() const;
virtual Duration airTime() const; // thoi gian chiem tren song
};
#endifcpp
#include "Track.h"
using namespace std;
Track::Track(const string& title, const Duration& length)
: title(title), length(length) {}
Track::~Track() {}
string Track::getTitle() const { return title; }
Duration Track::airTime() const { return length; }cpp
#ifndef SONG_H
#define SONG_H
#include "Track.h"
class Song : public Track {
public:
Song(const std::string& title, const Duration& length);
Duration airTime() const override; // nghi 5 giay
};
#endifcpp
#include "Song.h"
using namespace std;
Song::Song(const string& title, const Duration& length) : Track(title, length) {}
Duration Song::airTime() const { return length + Duration(0, 5); }cpp
#ifndef PODCAST_H
#define PODCAST_H
#include "Track.h"
class Podcast : public Track {
public:
Podcast(const std::string& title, const Duration& length);
Duration airTime() const override; // 30 giay quang cao
};
#endifcpp
#include "Podcast.h"
using namespace std;
Podcast::Podcast(const string& title, const Duration& length) : Track(title, length) {}
Duration Podcast::airTime() const { return length + Duration(0, 30); }cpp
#ifndef SLOT_H
#define SLOT_H
#include <string>
#include "Track.h"
#include "Duration.h"
class Slot { // mot khung phat trong lich
private:
std::string slotNo;
Track* track;
Duration start;
Duration end; // khong truyen vao: tinh tu start va track
public:
Slot(const std::string& slotNo, Track* track, const Duration& from);
Duration getEnd() const;
void print() const;
};
#endifcpp
#include "Slot.h"
#include <iostream>
using namespace std;
Slot::Slot(const string& slotNo, Track* track, const Duration& from)
: slotNo(slotNo), track(track), start(from),
end(start + track->airTime()) {} // start o day la THUOC TINH vua khoi tao ngay tren
Duration Slot::getEnd() const { return end; }
void Slot::print() const {
cout << " " << slotNo << " | " << track->getTitle() << " | "
<< start << " - " << end << endl;
}cpp
#include "Song.h"
#include "Podcast.h"
#include "Slot.h"
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<Track*> playlist;
playlist.push_back(new Song("Hello Viet Nam", Duration(4, 23)));
playlist.push_back(new Podcast("Chuyen lap trinh", Duration(12, 0)));
playlist.push_back(new Song("Mua he", Duration(3, 35)));
cout << "Danh sach co " << playlist.size() << " ban ghi" << endl;
vector<Slot> schedule;
Duration now(0, 0);
for (Track* t : playlist) {
Slot s("K" + to_string(schedule.size() + 1), t, now);
schedule.push_back(s);
now = s.getEnd();
}
cout << "--- LICH PHAT ---" << endl;
for (const Slot& s : schedule)
s.print();
for (Track* t : playlist)
delete t; // vector chi giu con tro, khong tu delete ban ghi
return 0;
}text
Danh sach co 3 ban ghi
--- LICH PHAT ---
K1 | Hello Viet Nam | 0:00 - 4:28
K2 | Chuyen lap trinh | 4:28 - 16:58
K3 | Mua he | 16:58 - 20:38std::vector. Cần #include <vector>.
vector<Track*> playlist;là một mảng tự giãn:push_backthêm vào cuối,size()cho số phần tử, không cần tựnew[]haydelete[]nhưLibrarytuần 5.for (Track* t : playlist)duyệt lần lượt từng phần tử.- Vector chỉ lo chỗ chứa con trỏ. Đối tượng tạo bằng
newvẫn phải tựdelete, nên cuốimaincó vòng lặpdelete t. vector<Slot> schedulechứa thẳng đối tượng:push_back(s)chépsvào vector. Duyệt bằngfor (const Slot& s : schedule)để khỏi chép thêm lần nữa.
Thuộc tính là đối tượng của lớp khác. Track có thuộc tính Duration length; Slot có Duration start và Duration end. Thuộc tính đó khởi tạo trong danh sách khởi tạo như mọi thuộc tính khác, và có thể tính từ các thuộc tính đứng trước nó: end(start + track->airTime()). Ở đây start là thuộc tính vừa khởi tạo ngay dòng trên (tham số tên là from), còn track là tham số. airTime() là hàm ảo, nên Slot không cần biết mình đang giữ bài hát hay podcast.
Thứ tự khởi tạo là thứ tự khai báo. Thuộc tính được khởi tạo theo thứ tự khai báo trong lớp, không theo thứ tự viết trong danh sách khởi tạo. Khai báo end trước start thì end(start + ...) dùng start khi nó chưa có giá trị. g++ cảnh báo:
text
In file included from Slot.cpp:1:
Slot.h: In constructor 'Slot::Slot(const std::string&, Track*, const Duration&)':
Slot.h:13:14: warning: 'Slot::start' will be initialized after [-Wreorder]
13 | Duration start;
| ^~~~~
Slot.h:12:14: warning: 'Duration Slot::end' [-Wreorder]
12 | Duration end; // SAI: khai bao end truoc start
| ^~~
Slot.cpp:5:1: warning: when initialized here [-Wreorder]
5 | Slot::Slot(const string& slotNo, Track* track, const Duration& from)
| ^~~~
Slot.cpp:7:11: warning: member 'Slot::start' is used uninitialized [-Wuninitialized]
7 | end(start + track->airTime()) {}
| ^~~~~Chạy thì giờ kết thúc sai hết, vì lúc tính end, thuộc tính start chưa có giá trị. Cảnh báo cuối cùng ở trên nói đúng điều đó: member 'Slot::start' is used uninitialized.
text
Danh sach co 3 ban ghi
--- LICH PHAT ---
K1 | Hello Viet Nam | 0:00 - 84:28
K2 | Chuyen lap trinh | 84:28 - 12:30
K3 | Mua he | 12:30 - 88:08Thấy cảnh báo -Wreorder thì sửa ngay thứ tự khai báo cho khớp thứ tự khởi tạo. Tính từ tham số thay vì thuộc tính, như end(from + track->airTime()), thì thứ tự khai báo không còn ảnh hưởng.
Bài 7.1 — Sửa lỗi của tuần trước bằng một từ khóa
Nhiệm vụ:
- Trong
Document.h, thêmvirtualtrướcdisplay(). - Ở
BookvàMagazine, thêmoverridevào cuối khai báodisplay():cppvoid display() const override;overridekhông bắt buộc về cú pháp, nhưng nếu bạn gõ sai tên hàm hoặc sai tham số, trình biên dịch sẽ báo lỗi ngay thay vì im lặng tạo ra một hàm mới. Luôn dùng nó. - Chạy lại đoạn code của bài 6.3. Bây giờ nó phải in
[SACH] .... - Tạo mảng
Document* ds[3]chứa mộtBook, mộtMagazine, mộtDocumentthường, duyệt và gọidisplay().
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>_w06 thành <MSSV>_w07, tải khung bài làm tuần 7 và chép bốn file trong đó vào thư mục mới, đè lên main.cpp và nhan_xet.txt cũ. Biên dịch g++ -std=c++17 -Wall *.cpp -o main.exe: chưa in gì nhưng phải qua.
Bước 2. Trong Document.h, thêm virtual trước void display() const;. Trong Book.h và Magazine.h, thêm override sau chữ const của display(). Chỉ sửa file .h (mục 1).
Bước 3. Làm TODO 1 trong main.cpp: Document* ds[3];, tạo ba phần tử bằng new với dữ liệu lấy từ kết quả mong đợi, rồi duyệt gọi ds[i]->display(). Biên dịch, so bốn dòng đầu.
Nếu cả ba dòng đều in [TL] ...: display() trong Document.h chưa có virtual, và lớp con cũng chưa có override để g++ nhắc (mục 1, mục 2).
Nếu gặp marked 'override', but does not override: lớp cha chưa có virtual, hoặc dòng khai báo ở lớp con lệch chữ ký với lớp cha, thường là thiếu const (mục 2). Một lần biên dịch thật khi quên virtual ở Document.h (trích):
text
In file included from Book.cpp:1:
Book.h:17:10: error: 'void Book::display() const' marked 'override', but does not override
17 | void display() const override;
| ^~~~~~~Bài 7.2 — Hạn mượn theo từng loại
Thêm vào Document: virtual int loanDays() const; trả về 7. Ghi đè ở lớp con: Sách 14 ngày, Tạp chí 3 ngày.
Cùng một lời gọi ds[i]->loanDays(), ba kết quả khác nhau — đó là đa hình.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Document.h: virtual int loanDays() const;. Document.cpp: trả về 7.
Bước 2. Book trả 14, Magazine trả 3; khai báo ở .h có override.
Bước 3. Phần in hạn trả cần LoanDate của Bài 7.3. Trong lúc chờ, tạm in ds[i]->loanDays() để thấy ba số 14, 3, 7.
Nếu cả ba hạn trả đều là 7 ngày: loanDays() trong Document.h chưa virtual, và lớp con không có override để g++ báo lỗi. Một lần chạy thật:
text
--- 7.2 HAN MUON THEO TUNG LOAI ---
Lap trinh huong doi tuong: muon 28/12/2026, han tra 04/01/2027 (7 ngay)
Vat ly Ngay nay: muon 28/12/2026, han tra 04/01/2027 (7 ngay)
Bao cao thuong nien: muon 28/12/2026, han tra 04/01/2027 (7 ngay)Bài 7.3 — Lớp LoanDate và nạp chồng toán tử


Nhiệm vụ:
- Hàm phụ
private:isLeap(y),daysInMonth(m, y),nextDay().nextDay()tiến một ngày và tự xử lý sang tháng, sang năm. Năm nhuận là năm chia hết cho 4 nhưng không chia hết cho 100, hoặc chia hết cho 400; tháng 2 năm nhuận có 29 ngày. operator+(int days)— trả về một đối tượng mới, không sửa đối tượng gốc. Cách đơn giản và chắc chắn đúng: sao chép rồi gọinextDay()đủdayslần.operator-(const LoanDate& other)— trả về số ngày giữa hai mốc.operator<<phải làfriendvà in dạngdd/mm/yyyy(có số 0 ở đầu khi cần). Trả vềostream&để còn nối tiếp đượccout << a << b.
Dùng ngay: với mỗi tài liệu, in ngày mượn và hạn trả = today + ds[i]->loanDays().
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Viết LoanDate.h theo sơ đồ; khung đã ghi sẵn từng TODO. operator<< khai báo trong lớp với chữ friend: friend std::ostream& operator<<(std::ostream& os, const LoanDate& d);.
Bước 2. isLeap và daysInMonth: dùng một mảng 12 số ngày; tháng 2 của năm nhuận có 29 ngày.
Bước 3. nextDay(): tăng day; vượt số ngày của tháng thì day về 1 và tăng month; month vượt 12 thì về 1 và tăng year.
Bước 4. operator+: khác Duration ở mục 4 là không cộng thẳng được. Chép *this vào một biến tạm, gọi nextDay() trên biến tạm đủ days lần, rồi trả biến tạm. Hàm có const.
Bước 5. operator-: chép mốc sớm hơn ra một biến tạm, đếm xem phải gọi nextDay() bao nhiêu lần thì biến tạm trùng ngày, tháng, năm với mốc muộn hơn.
Bước 6. Định nghĩa operator<< trong LoanDate.cpp không có LoanDate:: phía trước và không lặp lại chữ friend. In thêm 0 khi ngày hoặc tháng nhỏ hơn 10, cuối cùng return os; (mục 4).
Bước 7. Làm TODO 2 và TODO 3 trong main.cpp, so các dòng của phần 7.2 và 7.3.
Nếu hạn trả in ra 42/12/2026: nextDay() chưa sang tháng. Phép trừ cũng sai theo, vì mốc đang đếm không bao giờ trùng được ngày trả. Đáp án có chặn vòng lặp ở 100000 bước nên dừng ở 100001; không có chặn thì chương trình treo:
text
Lap trinh huong doi tuong: muon 28/12/2026, han tra 42/12/2026 (14 ngay)
Vat ly Ngay nay: muon 28/12/2026, han tra 31/12/2026 (3 ngay)
Bao cao thuong nien: muon 28/12/2026, han tra 35/12/2026 (7 ngay)
--- 7.3 NAP CHONG TOAN TU ---
Muon 20/12/2026, tra 05/01/2027 -> giu 100001 ngayNếu ngày mượn trôi dần theo từng dòng như dưới đây: operator+ đang sửa chính today thay vì trả về đối tượng mới (mục 4):
text
Lap trinh huong doi tuong: muon 11/01/2027, han tra 11/01/2027 (14 ngay)
Vat ly Ngay nay: muon 14/01/2027, han tra 14/01/2027 (3 ngay)
Bao cao thuong nien: muon 21/01/2027, han tra 21/01/2027 (7 ngay)Nếu gặp must have exactly one argument ở dòng khai báo operator<<: thiếu chữ friend (mục 4).
Nếu gặp invalid operands of types 'void' and 'const char [11]' to binary 'operator<<': operator<< đang trả void thay vì ostream& (mục 4).
Bài 7.4 — Đa hình phía bạn đọc
Thêm virtual cho maxBorrow() và display() của Reader, override ở hai lớp con. Duyệt mảng Reader* và gọi display().
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Trong Reader.h: virtual trước maxBorrow() và display(). Reader giờ có hàm ảo nên cũng cần destructor ảo, thân rỗng là đủ: virtual ~Reader() {} (mục 3).
Bước 2. Trong StudentReader.h và GradReader.h: thêm override cho cả maxBorrow() lẫn display().
Bước 3. Làm TODO 4: Reader* rs[3] tạo bằng new, duyệt gọi display(), so ba dòng của phần 7.4.
Nếu cả ba dòng đều là [BAN DOC] ... Han muc 3 tai lieu: Reader chưa có virtual. Một lần chạy thật:
text
[BAN DOC] BD001 | Nguyen Van An | Han muc 3 tai lieu
[BAN DOC] BD002 | Tran Thi Binh | Han muc 3 tai lieu
[BAN DOC] BD003 | Le Minh Cuong | Han muc 3 tai lieuBài 7.5 — Destructor ảo
- Thêm vào
Document:virtual ~Document();— thân hàm in(huy Document <id>). - Thêm vào
Book:~Book() override;— in(huy Book <id>). deletecác con trỏ và quan sát thứ tự in.- Thử nghiệm bắt buộc: bỏ chữ
virtualở destructor củaDocumentvà chữoverrideở destructor củaBook(bỏ mỗivirtualthì g++ báo'Book::~Book()' marked 'override', but does not override), chạy lại, ghi lại kết quả vàonhan_xet.txt, rồi trả cả hai về chỗ cũ. Bạn sẽ thấy destructor củaBookkhông chạy → rò rỉ bộ nhớ.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Document.h: virtual ~Document();. Document.cpp: thân destructor in dòng (huy Document <id>) đúng như kết quả mong đợi, có hai dấu cách ở đầu.
Bước 2. Book.h: ~Book() override;. Book.cpp: in dòng (huy Book <id>).
Bước 3. Làm TODO 5: in dòng tiêu đề, delete ba tài liệu rồi ba bạn đọc. So bốn dòng cuối: (huy Book S001) phải đứng trước (huy Document S001) (mục 3).
Bước 4. Thử nghiệm bắt buộc: bỏ virtual ở ~Document và override ở ~Book. Bỏ mỗi virtual thì chưa chạy được (trích):
text
In file included from Book.cpp:1:
Book.h:20:5: error: 'Book::~Book()' marked 'override', but does not override
20 | ~Book() override; // chay TRUOC destructor cua Document
| ^Bỏ cả hai thì biên dịch được, kèm cảnh báo -Wdelete-non-virtual-dtor. Cảnh báo có thể hiện ở cả Library.cpp, vì destructor của Library cũng delete qua Document*. Chép nguyên văn cảnh báo và phần 7.5 in ra vào nhan_xet.txt, trả lời ba câu hỏi trong file, rồi trả virtual và override về chỗ cũ.
Kết quả mong đợi
text
--- 7.1 CUNG MOT LOI GOI, KET QUA KHAC NHAU ---
[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
[TL] TL009 | Bao cao thuong nien | 2023 | Con 1 ban
--- 7.2 HAN MUON THEO TUNG LOAI ---
Lap trinh huong doi tuong: muon 28/12/2026, han tra 11/01/2027 (14 ngay)
Vat ly Ngay nay: muon 28/12/2026, han tra 31/12/2026 (3 ngay)
Bao cao thuong nien: muon 28/12/2026, han tra 04/01/2027 (7 ngay)
--- 7.3 NAP CHONG TOAN TU ---
Muon 20/12/2026, tra 05/01/2027 -> giu 16 ngay
--- 7.4 DA HINH BEN PHIA BAN DOC ---
[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
--- 7.5 HUY DOI TUONG (xem thu tu destructor) ---
(huy Book S001)
(huy Document S001)
(huy Document TC001)
(huy Document TL009)Hãy tự kiểm tra phép tính ngày: tháng 12 có 31 ngày, nên 28/12 + 14 phải ra 11/01 năm sau. Nếu chương trình bạn ra 42/12/2026 thì nextDay() đang sai.
Thư mục bài làm — giữ trên máy, không nộp
text
<MSSV>_w07/
Document.h/.cpp Book.h/.cpp Magazine.h/.cpp
Reader.h/.cpp StudentReader.h/.cpp GradReader.h/.cpp
LoanDate.h/.cpp
Library.h/.cpp
main.cpp
nhan_xet.txt (kết quả thử nghiệm bài 7.5)Bài tại lớp không nộp. Tuần 7 chỉ nộp bài về nhà ở cuối trang. Giữ nguyên thư mục này: tuần 8 làm bài lấy điểm ngay trên chính nó, nên tuần 7 thiếu gì thì bổ sung trước khi tới lớp.
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 |
virtual + override dùng đúng, đa hình hoạt động qua con trỏ | 2 |
loanDays() khác nhau theo loại | 1 |
Ba toán tử +, -, << đúng, operator<< là friend trả ostream& | 2 |
| Destructor ảo, thứ tự huỷ đúng | 1 |
nhan_xet.txt bài 7.5 đúng trọng tâm | 1 |
| Chia file, biên dịch một lệnh | 1 |
Bài về nhà — phần duy nhất phải nộp
Viết thêm cho LoanDate hai hàm so sánh hai mốc ngày, rồi dùng chúng để viết hàm tự do bool quaHan(const LoanDate& hanTra, const LoanDate& homNay).
Hợp đồng — máy chấm bám đúng ba chữ ký này
cpp
bool sameDay(const LoanDate& other) const; // ham thanh vien: hai moc co trung ngay khong
bool isBefore(const LoanDate& other) const; // ham thanh vien: moc nay co truoc moc kia khong
bool quaHan(const LoanDate& hanTra, const LoanDate& homNay); // ham tu doQuy ước: đúng ngày hạn thì chưa quá hạn, nên quaHan chỉ đúng khi hanTra nằm trước homNay, không phải trước hoặc trùng.
Chữ
constở cuối không được thiếu. Máy chấm so sánh qua tham chiếu hằngconst LoanDate&, nên cảsameDaylẫnisBefoređề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 và tuần 6.
Ba hàm trên là hàm thường, không phải toán tử. Bài 7.3 trên lớp vẫn luyện operator+, operator- và operator<<, vì đó là chỗ để hiểu cách nạp chồng hoạt động. Còn bài nộp lấy điểm thì chỉ cần hàm có tên: tên hàm nói rõ nó làm gì, và người đọc code không phải đoán a < b nghĩa là gì. Máy chấm không gọi tới ba toán tử của bài 7.3.
Nghĩa là ai chưa làm xong bài 7.3 vẫn làm được trọn 10 điểm bài này. Nộp nguyên tệp LoanDate của mình cũng được, mà dùng đúng hai tệp khung mở sẵn trên Moodle rồi viết ba hàm trên cũng được.
Chấ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 |
|---|---|
sameDay: trùng ngày, khác ngày, cùng ngày khác tháng, cùng ngày cùng tháng khác năm | 2,5 |
isBefore: cùng tháng, khác tháng, khác năm, hai mốc trùng nhau | 2,5 |
quaHan: còn hạn, đúng ngày hạn, quá hạn, quá hạn sang năm mới | 2,5 |
| Tình huống thư viện: ba tài liệu một ngày, và ngày 29/02 năm nhuận | 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 hai tệp:
LoanDate.h,LoanDate.cpp. - Không nộp
main.cpp: máy đã cómainriêng để chạy các kịch bản chấm. - Trong hoạt động Phòng lập trình ảo trên Moodle, hai tệp mở sẵn là một khung rỗng chỉ có mấy dòng
// TODO. Đã làm xong bài 7.3 thì mở file trên máy bạn, chép hết, dán đè vào từng tệp rồi thêm ba hàm — đừng gõ lại từ đầu trong trình duyệt. Chưa làm xong bài 7.3 thì cứ viết thẳng vào khung đó. - 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 tuần 8 của lớp bạn: lớp học thứ tư — 19:00 chủ nhật 15/11; lớp học thứ hai — 19:00 chủ nhật 22/11. 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 7 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.