Giao diện
Bài lab · Tuần 9
Tuần 9 — Chữa bài kiểm tra và tính trừu tượng
Hai phần dưới đây rời nhau và tuỳ lớp mà xếp buổi nào trước. Phần 1 chữa bài kiểm tra 15% trong 45 phút; phần 2 là nội dung mới —
Documenttrở thành lớp trừu tượng, thêm loại tài liệu thứ tư (Thesis), và một giao diệnRenewablecho những tài liệu gia hạn được.
Phần 1 — Chữa bài kiểm tra (45 phút)
Mở lại thư mục <MSSV>_kiemtra — thư mục bài kiểm tra quản lý thiết bị phòng thí nghiệm, không phải thư mục <MSSV>_w08. Giảng viên chữa lần lượt theo từng mục của thang điểm; bạn sửa ngay trên bài mình, đừng gõ lại từ đầu. Ai đã nhận điểm rồi thì sửa đúng những mục bị trừ.
Sửa cho chạy được là bắt buộc, không phải để lấy lại điểm. Đề kiểm tra là một đề riêng, không dùng lại thư mục thư viện, nên phần này không cản phần 2 — nhưng kỹ thuật bị trừ điểm ở đây sẽ gặp lại nguyên vẹn trong đề thi cuối kỳ.
Phần 2 — Tính trừu tượng
Mang gì sang
Chép <MSSV>_w08/ thành <MSSV>_w09/ — thư mục thư viện của phần 1 tuần 8, có Disc và Library đã chuyển sang vector. Thư mục <MSSV>_kiemtra của bài kiểm tra không liên quan tới đây, cứ để nguyên.
Mục tiêu
- Hiểu hàm thuần ảo (
= 0) và lớp trừu tượng. - Thấy được lớp trừu tượng ép mọi lớp con phải tự khai báo phần riêng của mình.
- Viết một giao diện (interface) và dùng đa kế thừa để "ký" giao diện đó.
- Kiểm chứng thiết kế: thêm một loại tài liệu mới mà không sửa lớp nào có sẵn.
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 trở thành lớp trừu tượng, có thêm loại tài liệu thứ tư và một giao diện Renewable. Ví dụ vẫn ở chuyện âm nhạc: Track giờ là lớp trừu tượng, thêm giao diện Karaoke cho những bản ghi hát karaoke được. 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. Riêng tuần này có thêm một điều: hàm thuần ảo= 0không có thân hàm nên nó không xuống.cpp, còn giao diện thì không có file.cppnào cả.
1. Hàm thuần ảo và lớp trừu tượng
Một bản ghi chung chung thì chưa có giá và cũng chưa biết mình thuộc loại gì; chỉ bài hát hay podcast cụ thể mới biết. Vì vậy Track chỉ khai báo hai hàm đó, không viết thân, và bắt mọi lớp con tự viết:


| Thấy trên UML | Viết trong C++ |
|---|---|
Tên lớp in nghiêng, ghi {abstract} | lớp có ít nhất một hàm thuần ảo |
Dòng in nghiêng + price() : int = 0 | virtual int price() const = 0; |
{interface} | lớp chỉ có hàm thuần ảo và destructor ảo, không có thuộc tính |
Mũi tên nét đứt, đầu tam giác rỗng từ Song lên Karaoke | class Song : public Track, public Karaoke |
Tám file, đúng cách bài thực hành bắt chia. Ba file đầu — giao diện và lớp trừu tượng: Có một điều chỉ lớp trừu tượng mới gặp: hàm thuần ảo không có thân hàm, nên nó không xuống .cpp — khai báo ở .h là xong. Giao diện thì toàn hàm thuần ảo, nên nó không có file .cpp nào cả.
cpp
#ifndef KARAOKE_H
#define KARAOKE_H
#include <string>
// GIAO DIEN: chi co ham thuan ao, khong co thuoc tinh, khong co file .cpp
class Karaoke {
public:
virtual int lineCount() const = 0;
virtual std::string firstLine() const = 0;
virtual ~Karaoke() = default;
};
#endifcpp
#ifndef TRACK_H
#define TRACK_H
#include <string>
// LOP TRUU TUONG: co ham thuan ao nen khong tao doi tuong duoc
class Track {
protected:
std::string title;
int seconds;
public:
Track(const std::string& title, int seconds);
virtual ~Track();
virtual int price() const = 0; // moi loai TU dinh gia
virtual std::string kind() const = 0; // moi loai TU khai ten loai
std::string getTitle() const;
void show() const; // ham thuong, dung hai ham thuan ao
};
#endifcpp
#include "Track.h"
#include <iostream>
using namespace std;
Track::Track(const string& title, int seconds) {
this->title = title;
this->seconds = seconds;
}
Track::~Track() {}
string Track::getTitle() const { return title; }
void Track::show() const {
cout << " " << title << " -> loai: " << kind() << ", gia " << price() << " dong" << endl;
}Track::show() gọi kind() và price() — hai hàm mà Track chưa hề viết. Nó vẫn biên dịch được, vì lúc chạy đối tượng thật luôn là một lớp con đã viết đủ hai hàm đó.
Năm file còn lại — hai lớp con và main:
cpp
#ifndef SONG_H
#define SONG_H
#include "Track.h"
#include "Karaoke.h"
// la mot Track, VA ky giao dien Karaoke
class Song : public Track, public Karaoke {
private:
int lines;
std::string first;
public:
Song(const std::string& title, int seconds, int lines, const std::string& first);
int price() const override;
std::string kind() const override;
int lineCount() const override;
std::string firstLine() const override;
};
#endifcpp
#include "Song.h"
using namespace std;
Song::Song(const string& title, int seconds, int lines, const string& first)
: Track(title, seconds) {
this->lines = lines;
this->first = first;
}
int Song::price() const { return 10000; }
string Song::kind() const { return "Bai hat"; }
int Song::lineCount() const { return lines; }
string Song::firstLine() const { return first; }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);
int price() const override;
std::string kind() const override;
};
#endifcpp
#include "Podcast.h"
using namespace std;
Podcast::Podcast(const string& title, int seconds, int episode)
: Track(title, seconds) {
this->episode = episode;
}
int Podcast::price() const { return 0; }
string Podcast::kind() const { return "Podcast"; }cpp
#include "Song.h"
#include "Podcast.h"
#include <iostream>
using namespace std;
int main() {
Track* ds[2];
ds[0] = new Song("Hello Viet Nam", 263, 32, "Xin chao Viet Nam");
ds[1] = new Podcast("Chuyen lap trinh", 1800, 12);
cout << "--- MOI LOAI TU KHAI BAO ---" << endl;
for (int i = 0; i < 2; i++)
ds[i]->show();
cout << "--- HAT KARAOKE DUOC KHONG? ---" << endl;
for (int i = 0; i < 2; i++) {
Karaoke* k = dynamic_cast<Karaoke*>(ds[i]); // doi tuong nay co ky giao dien Karaoke khong?
if (k == nullptr)
cout << " " << ds[i]->getTitle() << ": khong hat karaoke duoc." << endl;
else
cout << " " << ds[i]->getTitle() << ": " << k->lineCount()
<< " dong, dong dau: " << k->firstLine() << endl;
}
for (int i = 0; i < 2; i++)
delete ds[i];
return 0;
}Biên dịch cả tám file bằng một lệnh — .h không bao giờ đưa vào lệnh g++, nó đã được #include rồi:
sh
g++ -std=c++17 -Wall *.cpp -o main.exetext
--- MOI LOAI TU KHAI BAO ---
Hello Viet Nam -> loai: Bai hat, gia 10000 dong
Chuyen lap trinh -> loai: Podcast, gia 0 dong
--- HAT KARAOKE DUOC KHONG? ---
Hello Viet Nam: 32 dong, dong dau: Xin chao Viet Nam
Chuyen lap trinh: khong hat karaoke duoc.= 0biến hàm thành hàm thuần ảo:Trackkhông viết thân cho nó, lớp con nào muốn tạo được đối tượng thì phải viết.- Lớp có ít nhất một hàm thuần ảo là lớp trừu tượng. Nó vẫn có thuộc tính, constructor và hàm thường như
show(). show()không ảo nhưng gọi hai hàm thuần ảo, nên mỗi loại tự điền phần của mình vào cùng một dòng in.
2. Không tạo được đối tượng của lớp trừu tượng
Thử tạo một Track (trích):
text
main.cpp: In function 'int main()':
main.cpp:59:42: error: invalid new-expression of abstract class type 'Track'
59 | Track* t = new Track("Ban thu am", 95);
| ^
main.cpp:12:7: note: because the following virtual functions are pure within 'Track':
12 | class Track { // LOP TRUU TUONG: co ham thuan ao
| ^~~~~
main.cpp:21:17: note: 'virtual int Track::price() const'
21 | virtual int price() const = 0; // moi loai TU dinh gia
| ^~~~~
main.cpp:22:20: note: 'virtual std::string Track::kind() const'
22 | virtual string kind() const = 0; // moi loai TU khai ten loai
| ^~~~Các dòng note liệt kê đúng những hàm còn thuần ảo. Tạo biến thường cũng bị chặn:
text
main.cpp: In function 'int main()':
main.cpp:59:11: error: cannot declare variable 't' to be of abstract type 'Track'
59 | Track t("Ban thu am", 95);Còn con trỏ Track* thì vẫn dùng bình thường, như mảng Track* ds[2] ở trên: nó chỉ trỏ tới đối tượng của lớp con.
3. Lớp con quên viết một hàm thuần ảo
Lớp con đó cũng thành trừu tượng. Bỏ kind() của Podcast đi thì lỗi hiện ra ở chỗ new Podcast, không phải trong lớp Podcast; dòng note cho biết còn thiếu hàm nào:
text
main.cpp: In function 'int main()':
main.cpp:60:53: error: invalid new-expression of abstract class type 'Podcast'
60 | ds[1] = new Podcast("Chuyen lap trinh", 1800, 12);
| ^
main.cpp:46:7: note: because the following virtual functions are pure within 'Podcast':
46 | class Podcast : public Track {
| ^~~~~~~
main.cpp:22:20: note: 'virtual std::string Track::kind() const'
22 | virtual string kind() const = 0; // moi loai TU khai ten loai
| ^~~~Đây chính là điều lớp trừu tượng làm được: thêm một loại mới mà quên khai phần riêng thì g++ nhắc ngay.
4. Giao diện và đa kế thừa
Karaoke là một giao diện: chỉ có hàm thuần ảo và destructor ảo, không có dữ liệu. Nó không nói bản ghi "là gì", mà nói bản ghi "biết làm gì".
class Song : public Track, public Karaoke: bài hát là một bản ghi, và biết hát karaoke.Podcastkhông ký giao diện này.dynamic_cast<Karaoke*>(ds[i])hỏi lúc chạy: đối tượng màds[i]trỏ tới có kýKaraokekhông? Có thì trả về con trỏ dùng được, không thì trảnullptr.- Luôn kiểm tra
nullptrtrước khi gọi hàm qua con trỏ vừa đổi.
5. Hai lỗi âm thầm với giao diện
Quên public trước tên giao diện. Viết class Song : public Track, Karaoke thì vẫn biên dịch không một cảnh báo, nhưng cả bài hát cũng trả lời là không hát được:
text
--- HAT KARAOKE DUOC KHONG? ---
Hello Viet Nam: khong hat karaoke duoc.
Chuyen lap trinh: khong hat karaoke duoc.Thiếu public, Song kế thừa Karaoke kiểu riêng tư: bên ngoài lớp không ai thấy Song đã ký giao diện, nên dynamic_cast trả nullptr.
Không kiểm tra nullptr. Gọi thẳng k->lineCount() thì chương trình dừng im lặng khi tới podcast, như lỗi con trỏ của tuần 5:
text
--- MOI LOAI TU KHAI BAO ---
Hello Viet Nam -> loai: Bai hat, gia 10000 dong
Chuyen lap trinh -> loai: Podcast, gia 0 dong
--- HAT KARAOKE DUOC KHONG? ---
Hello Viet Nam: 32 dong, dong dau: Xin chao Viet NamBài 9.1 — Biến Document thành lớp trừu tượng
Nhiệm vụ:
Trong
Document.h, đổiloanDays()thành hàm thuần ảo và thêm một hàm thuần ảo nữa:cppvirtual int loanDays() const = 0; virtual std::string typeName() const = 0;Xoá phần cài đặt
Document::loanDays()trongDocument.cpp.Thêm
typeName()cho cả ba lớp con:"Sach","Tap chi","Dia".Thử nghiệm bắt buộc. Thêm dòng này vào
mainrồi biên dịch:cppDocument* d = new Document("TL009", "Bao cao", 2023, 1);Nó sẽ không biên dịch được. Chép nguyên thông báo lỗi vào
nhan_xet.txt. Bạn sẽ thấy g++ nói gần đúng như sau:cpperror: invalid new-expression of abstract class type 'Document' note: because the following virtual functions are pure within 'Document': 'virtual int Document::loanDays() const' 'virtual std::string Document::typeName() const'Sau khi ghi lại, hãy comment dòng đó lại.
Giải thích trong 2–3 dòng: vì sao ngăn không cho tạo
Documentlại là điều tốt cho hệ thống thư viện?
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Nửa đầu buổi chữa bài tuần 8 ngay trong thư mục của bạn. Xong thì chép <MSSV>_w08 thành <MSSV>_w09, tải khung bài làm tuần 9 và chép năm file trong đó vào, đè lên main.cpp 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, đổi loanDays() thành virtual int loanDays() const = 0; và thêm virtual std::string typeName() const = 0;. Xoá thân Document::loanDays() trong Document.cpp.
Bước 3. Thêm typeName() cho Book, Magazine, Disc: khai báo trong .h có override, thân trong .cpp trả "Sach", "Tap chi", "Dia".
Bước 4. Làm TODO 0 trong main.cpp: thêm dòng tạo Document của đề rồi biên dịch. Chép nguyên văn thông báo lỗi vào nhan_xet.txt, kể cả các dòng note liệt kê hai hàm thuần ảo, rồi comment dòng đó lại (mục 2).
Bước 5. Làm TODO 1 và TODO 2 với ba tài liệu đã có; LV001 thêm sau Bài 9.2. Biên dịch, so các dòng của phần 9.1.
Nếu gặp invalid new-expression of abstract class type 'Disc' (hoặc 'Book', 'Magazine'): lớp con đó chưa viết đủ hàm thuần ảo, hoặc viết lệch chữ ký, thường là thiếu const. Dòng note cuối cho biết hàm nào (mục 3):
text
main.cpp: In function 'int main()':
main.cpp:18:65: error: invalid new-expression of abstract class type 'Disc'
18 | lib.add(new Disc("D001", "Thi nghiem quang hoc", 2022, 1, 45));
| ^
In file included from main.cpp:4:
Disc.h:7:7: note: because the following virtual functions are pure within 'Disc':
7 | class Disc : public Document {
| ^~~~
In file included from Library.h:6,
from main.cpp:1:
Document.h:34:25: note: 'virtual std::string Document::typeName() const'
34 | virtual std::string typeName() const = 0; // thuan ao: ten loai tai lieu
| ^~~~~~~~Nếu gặp lỗi đó với chính Document ở một dòng khác dòng thử nghiệm: còn chỗ nào đó tạo Document, ví dụ đoạn new Document(...) sót lại từ tuần 7. Xoá chỗ đó đi.
Bài 9.2 — Thêm loại tài liệu thứ tư: Thesis (luận văn)
- Thuộc tính riêng:
supervisor(giảng viên hướng dẫn),major(chuyên ngành). loanDays()trả về 7,typeName()trả về"Luan van".display()in dạng[LUAN VAN] ...
Đây là bài kiểm tra thiết kế của bạn. Nếu các tuần trước làm đúng, thêm lớp này không phải sửa một dòng nào trong
Document,Library,LoanSliphaymain(ngoài dònglib.add(new Thesis(...))). Nếu bạn buộc phải sửa các lớp cũ, hãy ghi vàonhan_xet.txtchỗ nào phải sửa và vì sao.Ở tuần 6 bạn đã bấm giờ khi thêm lớp
Newspaper. So lại: lần này nhanh hơn hay chậm hơn?
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Làm TODO 1 và TODO 2 trong Thesis.h: kế thừa Document, hai thuộc tính supervisor, major, constructor sáu tham số, và ba hàm display, loanDays, typeName có override.
Bước 2. Trong Thesis.cpp: constructor gọi Document(...) trước; loanDays() trả 7, typeName() trả "Luan van"; display() theo dòng [LUAN VAN] của kết quả mong đợi, để ý chữ GVHD đứng trước tên giảng viên.
Bước 3. Trong main, thêm LV001 bằng lib.add(new Thesis(...)), rồi thêm mã LV001 vào vòng lặp của phần 9.1. Biên dịch, so dòng [LUAN VAN] và dòng LV001 -> loai: Luan van, han muon 7 ngay.
Bước 4. Trả lời câu 3 và 4 trong nhan_xet.txt: có phải sửa Document, Library hay LoanSlip không, và mất bao nhiêu phút so với lần thêm Newspaper ở tuần 6.
Bài 9.3 — Giao diện Renewable
Sách và luận văn thì gia hạn được; tạp chí và đĩa thì không. Đây không phải chuyện kế thừa — nó là chuyện "có biết làm việc này hay không". Dùng giao diện:
cpp
// Renewable.h - chi co ham thuan ao, khong co du lieu
class Renewable {
public:
virtual bool renew(int days) = 0;
virtual int renewLeft() const = 0;
virtual ~Renewable() = default;
};Nhiệm vụ:
class Book : public Document, public Renewable— sách gia hạn tối đa 2 lần.class Thesis : public Document, public Renewable— luận văn gia hạn tối đa 1 lần.MagazinevàDisckhông kế thừaRenewable.- Trong
main, duyệt bốn tài liệu và hỏi từng cái có gia hạn được không:cppRenewable* r = dynamic_cast<Renewable*>(d); if (r == nullptr) { /* khong gia han duoc */ } else { /* r->renewLeft(), r->renew(7) */ }dynamic_casthỏi lúc chạy: đối tượng này có ký giao diệnRenewablekhông? Trảnullptrnghĩa là không.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Renewable.h trong khung đã đầy đủ. Book.h và Thesis.h phải #include "Renewable.h".
Bước 2. class Book : public Document, public Renewable, nhớ chữ public thứ hai (mục 5). Thêm một thuộc tính đếm số lần đã gia hạn, khởi tạo bằng 0 trong danh sách khởi tạo. renew(days): đã đủ 2 lần thì trả false; còn lượt thì tăng bộ đếm, in dòng Gia han them <days> ngay. thụt bốn dấu cách như kết quả mong đợi, trả true. renewLeft() trả số lượt còn lại.
Bước 3. Làm TODO 3 trong Thesis: như Book nhưng tối đa 1 lần.
Bước 4. Làm TODO 3 trong main: với từng mã, dynamic_cast<Renewable*>(d), kiểm tra nullptr trước (mục 4), rồi in theo kết quả mong đợi. Biên dịch, so toàn bộ 19 dòng.
Nếu sách in khong gia han duoc: Book.h thiếu chữ public trước Renewable (mục 5). Một lần chạy thật:
text
--- 9.2 TAI LIEU NAO GIA HAN DUOC? ---
Lap trinh huong doi tuong: khong gia han duoc.Nếu chương trình dừng ngay sau dòng gia hạn của sách mà không báo lỗi: main chưa kiểm tra nullptr, nên chết khi tới tạp chí:
text
--- 9.2 TAI LIEU NAO GIA HAN DUOC? ---
Lap trinh huong doi tuong: con 2 lan gia han.
Gia han them 7 ngay.
-> con lai 1 lan.Nếu số lần gia hạn in ra một số lạ: quên khởi tạo bộ đếm bằng 0. Giá trị lúc đó là rác, có lần chạy tình cờ vẫn ra đúng, nên đừng lấy một lần chạy đúng làm bằng chứng.
Kết quả mong đợi
text
--- DANH MUC Thu vien Khoa ---
[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
[DIA] D001 | Thi nghiem quang hoc | 2022 | 45 phut | Con 1 ban
[LUAN VAN] LV001 | Mang nano TiO2 | GVHD TS. Nguyen Van B | Vat ly chat ran | 2025 | Con 1 ban
--- 9.1 MOI LOAI TU KHAI BAO MINH LA GI ---
S001 -> loai: Sach, han muon 14 ngay
TC001 -> loai: Tap chi, han muon 3 ngay
D001 -> loai: Dia, han muon 2 ngay
LV001 -> loai: Luan van, han muon 7 ngay
--- 9.2 TAI LIEU NAO GIA HAN DUOC? ---
Lap trinh huong doi tuong: con 2 lan gia han.
Gia han them 7 ngay.
-> con lai 1 lan.
Vat ly Ngay nay: khong gia han duoc.
Thi nghiem quang hoc: khong gia han duoc.
Mang nano TiO2: con 1 lan gia han.
Gia han them 7 ngay.
-> con lai 0 lan.Thư mục bài làm — giữ trên máy, không nộp
text
<MSSV>_w09/
(toàn bộ file tuần 8, đã sửa)
Renewable.h
Thesis.h/.cpp
nhan_xet.txt (thông báo lỗi bài 9.1 + trả lời bài 9.2)Bài tại lớp không nộp. Tuần 9 chỉ nộp bài về nhà ở cuối trang. Giữ nguyên thư mục này: tuần 10 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 |
|---|---|
| Tính trừu tượng: thiết kế lớp trừu tượng đúng | 2 |
| Bốn lớp con cài đặt đủ hàm thuần ảo | 2 |
Giao diện Renewable và dynamic_cast hoạt động đúng | 2 |
| Chương trình chạy đúng | 2 |
nhan_xet.txt trả lời đú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
Thêm int Library::demTheoLoai(const std::string& loai) const — đếm số tài liệu có typeName() trùng với chuỗi truyền vào. Gọi thử với "Sach" và "Luan van". Hạn nộp: trước buổi tuần 10. Nén các file đã sửa và main.cpp thành <MSSV>_w09_bvn.zip rồi nộp lên Moodle.
Đáp án tuần này
Nộp bài xong rồi mở trang đáp án tuần 9 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.