Giao diện
Bài lab · Tuần 8
Tuần 8 — Phần 1: vector, loại tài liệu thứ ba và ôn tập
Tuần này thêm vào hệ thống thư viện:
Librarybỏ mảng con trỏ tự quản lý, chuyển sangstd::vector; thêm loại tài liệu thứ ba làDisc. Phần cuối giờ ôn lại đóng gói và UML.
Phần 1 không chấm điểm, nhưng không được bỏ. Mọi tuần sau làm tiếp trên chính thư mục này: thiếu
vectorhoặc thiếuDiscthì tuần 9 và tuần 10 không chạy được. Bài kiểm tra 15% (Phần 2) là một đề riêng, không liên quan tới thư mục này — lớp học thứ tư làm ngày 18/11, lớp học thứ hai ngày 23/11.
Mang gì từ tuần trước
Chép <MSSV>_w07/ thành <MSSV>_w08/. Bạn đã có Document, Book, Magazine, Reader và hai lớp con, LoanDate, Library.
Lý thuyết cần nắm
Tuần này chỉ có một điều mới: std::vector. Nó thay cho đoạn tự quản lý mảng động mà bạn viết từ tuần 5 — ba thuộc tính items, count, capacity rút về một dòng. Phần còn lại của buổi là ôn: thêm một loại tài liệu nữa để thấy kế thừa và đa hình đã dựng đúng chưa, rồi soi lại đóng gói và UML.
Ví dụ vẫn ở chuyện âm nhạc như tuần 6 và 7: Track, Song, Podcast. Mọi đoạn code dưới đây đều đã biên dịch và chạy thật, phần in ra là kết quả thật chứ không phải chép tay.
Về việc chia
.hvà.cpp. Các trang lý thuyết trước gom cả lớp vào một đoạn code cho gọn mắt, trong khi bài thực hành lại bắt tách khai báo sang.hvà thân hàm sang.cpp— nhiều bạn phản ánh là lệch nhau, và đúng là lệch. Từ trang này trở đi, lớp nào đáng chia thì hiện ra đúng hai file như khi bạn làm bài: xem mục 4 và mục 5. Hai chương đầu không chia, vì lúc đó chưa có lớp nào để chia.
1. std::vector: mảng tự lo chỗ chứa
vector là một mảng biết tự lớn lên. Bạn không cần biết trước sẽ có bao nhiêu phần tử, không cần new, không cần delete[], không có chuyện "kho đầy".
cpp
#include <vector> // bat buoc
vector<int> so; // tao rong, chua co phan tu nao
so.push_back(7); // them vao cuoi
so.push_back(3);
so.push_back(9);
cout << "so phan tu: " << so.size() << endl;
cout << "phan tu dau: " << so[0] << endl;
for (int x : so)
cout << " " << x << endl;text
so phan tu: 3
phan tu dau: 7
7
3
9Ba việc bạn cần, chỉ ba thôi:
| Việc | Viết |
|---|---|
| Khai báo | vector<Kiểu> tên; |
| Thêm vào cuối | tên.push_back(giá_trị); |
| Duyệt hết | for (Kiểu x : tên) |
Lấy phần tử theo chỉ số vẫn là tên[i] như mảng thường, và tên.size() cho biết đang có bao nhiêu.
size()trả về kiểu không dấu. Viếtfor (int i = 0; i < ds.size(); i++)thì g++ cảnh báo:textwarning: comparison of integer expressions of different signedness: 'int' and 'std::vector<Track*>::size_type' {aka 'long long unsigned int'} [-Wsign-compare]Để so được, g++ phải đổi
intsang không dấu — và mộtintâm khi đó biến thành một số rất lớn. Dùngfor (Track* t : ds)là hết chuyện; thật sự cần chỉ số thì khaisize_t i.
2. Phải là vector<Track*>, không phải vector<Track>
Đây là chỗ quyết định. vector<Track> chứa đối tượng, nên push_back một Song vào đó chỉ chép được phần Track nằm trong Song — phần riêng của lớp con bị cắt mất. Hiện tượng này gọi là slicing.
cpp
cout << "--- vector<Track*> : dung con tro ---" << endl;
vector<Track*> a;
a.push_back(new Song("Dieu con mai", 245, "Hong Nhung"));
for (Track* t : a) t->show();
for (Track* t : a) delete t;
cout << "--- vector<Track> : chep theo gia tri ---" << endl;
vector<Track> b;
Song s("Dieu con mai", 245, "Hong Nhung");
b.push_back(s); // chi chep duoc phan Track nam trong Song
for (const Track& t : b) t.show();text
--- vector<Track*> : dung con tro ---
[BAI HAT] Dieu con mai | Hong Nhung | 245 giay
--- vector<Track> : chep theo gia tri ---
[BAN GHI] Dieu con mai | 245 giayCùng một bài hát, cùng một lời gọi show(), hai kết quả khác nhau. Dòng dưới mất tên ca sĩ và in [BAN GHI], vì cái nằm trong b không còn là Song nữa.
Đa hình chỉ chạy qua con trỏ hoặc tham chiếu tới lớp cha. Chép theo giá trị là mất. Đây là câu hỏi quen mặt trong đề thi, và cũng là lý do mọi
Library,Playlist,LabRoom... đều chứavector<Cha*>.
3. vector dọn chỗ chứa, nó không dọn đối tượng
vector sở hữu mảng con trỏ, không sở hữu những thứ con trỏ trỏ tới. Hết phạm vi, nó trả lại chỗ chứa ba con trỏ; ba đối tượng tạo bằng new vẫn nằm nguyên trên heap, không ai gọi destructor của chúng.
cpp
vector<Track*> ds;
ds.push_back(new Song("Dieu con mai", 245, "Hong Nhung"));
ds.push_back(new Podcast("Vat ly moi ngay", 1800, "Minh Tri"));
ds.push_back(new Track("Tieng mua", 120));
for (Track* t : ds)
t->show();
for (Track* t : ds)
delete t; // vector khong lam viec nay ho bantext
[BAI HAT] Dieu con mai | Hong Nhung | 245 giay
[PODCAST] Vat ly moi ngay | dan: Minh Tri | 1800 giay
[BAN GHI] Tieng mua | 120 giayQuy tắc gọn: new của ai thì delete của người đó. Lớp nào new đối tượng rồi cất vào vector thì destructor của lớp đó phải có vòng delete.
Bỏ vòng
deletethì chương trình in ra y hệt bài đúng. Không lỗi, không cảnh báo, chỉ rò rỉ bộ nhớ — đọc code mới thấy. Chấm bài là đọc code, nên mục này mất điểm rất lặng lẽ.
Và nhớ delete, không phải delete[]: delete[] dành cho mảng bạn tự xin bằng new Kiểu[n]. Chuyển sang vector rồi thì không còn mảng nào do bạn xin nữa, nên không còn delete[] nào hết.
4. Thêm loại thứ ba mà không phải sửa lớp cha
Nếu tuần 6 và 7 bạn dựng đúng, thêm một lớp con nữa là chuyện của hai file mới, không đụng tới lớp cha và không đụng tới bất kỳ vòng lặp nào. Khai báo nằm ở .h, thân hàm nằm ở .cpp — đúng như bài thực hành bắt làm. Bấm vào tên file để xem từng file:
cpp
#ifndef AUDIOBOOK_H
#define AUDIOBOOK_H
#include "Track.h"
class Audiobook : public Track {
private:
std::string reader;
public:
Audiobook(const std::string& title, int seconds, const std::string& reader);
void show() const override;
int price() const override;
};
#endifcpp
#include "Audiobook.h"
#include <iostream>
using namespace std;
Audiobook::Audiobook(const string& title, int seconds, const string& reader)
: Track(title, seconds) { // goi constructor lop cha
this->reader = reader;
}
void Audiobook::show() const { // KHONG viet lai chu override o day
cout << "[SACH NOI] " << title << " | doc: " << reader
<< " | " << seconds << " giay" << endl;
}
int Audiobook::price() const { return 30000; }Ba chỗ hay sai khi chia file.
virtualvàoverridechỉ viết ở.h, chép sang.cpplà lỗi biên dịch. Tên hàm trong.cppphải cóAudiobook::ở trước, thiếu là bạn vừa viết một hàm tự do trùng tên. Và.hnào cũng phải có#ifndef/#define/#endif, thiếu thì include hai lần là lỗi "redefinition".
Vòng lặp cũ không sửa một chữ, mà kết quả có thêm loại mới:
text
[BAI HAT] Dieu con mai | Hong Nhung | 245 giay
[PODCAST] Vat ly moi ngay | dan: Minh Tri | 1800 giay
[SACH NOI] Nha gia kim | doc: Thu Ha | 21600 giay
--- GIA TAI VE ---
Dieu con mai: 12000 dong
Vat ly moi ngay: 0 dong
Nha gia kim: 30000 dongBa lớp con, ba price() khác nhau, một lời gọi t->price() duy nhất. Nếu để thêm một loại mà bạn phải mở lớp cha ra sửa, hoặc phải thêm một nhánh if vào vòng lặp, thì thiết kế đang sai chỗ nào đó.
5. Ôn đóng gói: setter có kiểm tra, getter có const
Hai điều hay bị mất điểm, cả hai đều kiểm tra được trong ba phút:
Setter phải kiểm tra, không phải chỉ gán. void setYear(int y) { year = y; } không phải đóng gói — nó chỉ là cái public viết dài dòng. Có kiểm tra mới là đóng gói. Và vì đã chia file nên nó nằm ở hai chỗ — một dòng khai báo, một thân hàm:
cpp
void setSeconds(int s); // co kiem tracpp
void Track::setSeconds(int s) {
if (s <= 0 || s > 86400) {
cout << " Thoi luong khong hop le!" << endl;
return; // tu choi, giu nguyen gia tri cu
}
seconds = s;
}Gọi ds[0]->setSeconds(-5) rồi show() thì thấy giá trị cũ còn nguyên:
text
Thoi luong khong hop le!
[BAN GHI] Tieng mua | 120 giayGetter chỉ đọc thì phải có const ở cuối. string getTitle() const { return title; }. Thiếu chữ const là không biên dịch được ở chỗ gọi qua tham chiếu hằng, chứ không phải chạy hơi sai. Bẫy này đã lấy điểm ở bài về nhà tuần 4, tuần 6 và tuần 7.
6. Đọc UML cho vector
Thuộc tính chứa một vector con trỏ thì ghi đúng như kiểu trong code:
| Thấy trên UML | Viết trong file .h |
|---|---|
- items : vector<Document*> | std::vector<Document*> items; |
+ add(Document*) : void | void add(Document* d); |
+ {virtual} display() : void | virtual void display() const; |
+ display() : void {override} ở lớp con | void display() const override; |
Đề thi cho sẵn sơ đồ UML rồi bắt viết code từ đó, nên đọc được UML là điểm thật.
Bài 8.1 — Library chuyển sang std::vector (25 phút)
Mục tiêu 25 phút25:00Chưa bắt đầu
Khai báo cũ của bạn phải giữ ba thứ chỉ để tự quản lý mảng:
cpp
Document** items; // mang con tro
int count; // dang co bao nhieu
int capacity; // suc chua toi daBa dòng đó thay bằng một dòng, nhớ #include <vector>:
cpp
std::vector<Document*> items;Sửa tiếp cho khớp:
| Chỗ | Trước | Sau |
|---|---|---|
| Constructor | Library(name, address, capacity) | Library(const std::string& name) — bỏ cả address lẫn capacity |
add | bool add(Document* d) | void add(Document* d), thân hàm là items.push_back(d); |
| Duyệt | for (int i = 0; i < count; i++) | for (Document* d : items) |
lend | void lend(const string& id) | bool lend(const string& id) — trả true nếu mượn được |
| Destructor | delete items[i] rồi delete[] items | for (Document* d : items) delete d; — không còn delete[] |
vectordọn chỗ chứa con trỏ, nó không dọn đối tượng mà con trỏ trỏ tới. Bỏdeletetrong destructor là rò rỉ toàn bộ tài liệu đãnew. Đây là câu hay bị hỏi trong đề thi.
Năm chuỗi in phải đúng tới từng dấu cách, vì từ tuần sau bài của bạn được so với kết quả mẫu từng dòng một:
cpp
cout << "--- DANH MUC " << name << " ---" << endl; // listAll
cout << " Da cho muon: " << d->getTitle() << " (con " << d->getCopies() << " ban)" << endl;
cout << " Da nhan tra: " << d->getTitle() << " (con " << d->getCopies() << " ban)" << endl;
cout << " Het ban: " << d->getTitle() << endl;
cout << " Khong tim thay ma " << id << "!" << endl;Bốn dòng của lend và receive thụt vào hai dấu cách, dòng listAll thì không. Nhân đây bỏ luôn dòng cout trong destructor của Library, Document và Book — tuần 7 để đó chỉ để quan sát thứ tự huỷ, từ giờ nó làm output bẩn.
Chạy lại chương trình tuần 7: danh mục phải in ra y như cũ, chỉ khác dòng tiêu đề. Đổi hẳn cách chứa dữ liệu bên trong mà bên ngoài gần như không phải sửa gì — đó là đóng gói đang trả công cho bạn.
Tự kiểm tra — ghi câu trả lời vào nhan_xet.txt
items.size()trả về kiểu gì? Vì saofor (int i = 0; i < items.size(); i++)bị g++ cảnh báo?vectortự dọn, vậy vì sao destructor vẫn phảidelete?- Nếu khai báo
vector<Document> items;— không có dấu*— thì đa hình còn chạy không? Thử thêm mộtBookvào kiểu vector đó rồi gọidisplay(), ghi lại những gì bạn thấy.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Đừng sửa cả Library một lượt rồi mới biên dịch. Sửa tới đâu biên dịch tới đó:
- Thêm
#include <vector>vàoLibrary.h, đổi ba thuộc tính thànhstd::vector<Document*> items;. - Sửa constructor trước — bỏ cả
addresslẫncapacity. Biên dịch: lúc nàymain.cppsẽ báo lỗi ở dòng tạoLibrary, sửa luôn dòng đó. - Sửa
addthànhvoid, thân hàm một dòngitems.push_back(d);. - Đổi mọi vòng
for (int i = 0; i < count; i++)thànhfor (Document* d : items). - Sửa destructor:
for (Document* d : items) delete d;, xoá dòngdelete[] items. - Đổi
lendsang trảbool. Chưa đổi thì g++ báo ở chỗ dùng chứ không phải chỗ khai báo:could not convert ... from 'void' to 'bool'. - Bỏ các dòng
couttrong destructor củaLibrary,DocumentvàBook— tuần 7 để đó chỉ để quan sát thứ tự huỷ, từ giờ nó làm output bẩn.
Chạy lại: danh mục phải in ra y như tuần 7. Đổi hẳn cách chứa dữ liệu bên trong mà bên ngoài gần như không phải sửa gì — đó là đóng gói đang trả công cho bạn.
Bài 8.2 — Loại tài liệu thứ ba: Disc (15 phút)
Mục tiêu 15 phút15:00Chưa bắt đầu
Đĩa phim/tư liệu. Kế thừa Document, thêm đúng một thuộc tính riêng:
int minutes— thời lượng đĩa.display()in dạng[DIA] <ma> | <tua> | <nam> | <so phut> phut | Con <n> banloanDays()trả về 2 — đĩa chỉ cho mượn hai ngày.
Viết theo đúng khuôn Magazine của tuần 6: constructor gọi constructor lớp cha trong danh sách khởi tạo, display() và loanDays() đều có override.
Sửa main() cho kho còn đúng ba tài liệu — bỏ S002 nếu bạn đang có — rồi chạy:
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 banD001 là Thi nghiem quang hoc, năm 2022, 1 bản, 45 phút. Kiểm tra thêm hạn trả — đĩa phải khác sách và tạp chí:
text
Thi nghiem quang hoc: muon 28/12/2026, han tra 30/12/2026 (2 ngay)Ba lớp con, ba
loanDays()khác nhau, một lời gọids[i]->loanDays()duy nhất. ThêmDiscmà không phải sửaDocument,Libraryhay vòng lặp nào — đó là thước đo thiết kế của bạn.
Trả lời ngay trên code của mình
Discgọi constructor củaDocumentở chỗ nào? Chỉ đúng dòng đó ra.- Nếu bỏ chữ
overrideởdisplay()thì g++ nói gì? Nếu gõ sai thànhdispaly()thì sao? ~Document()của bạn cóvirtualkhông? Không có thìdeletemộtDocument*trỏ tớiDiscsẽ hỏng chỗ nào?
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Chép Magazine.h/Magazine.cpp ra thành Disc.h/Disc.cpp rồi sửa, nhanh hơn gõ lại. Ba chỗ phải sửa: tên lớp (cả trong #ifndef), thuộc tính riêng đổi thành int minutes, và hai thân hàm display() với loanDays().
Nhớ bỏ S002 trong main() cho kho còn đúng ba tài liệu — tuần 9 so kết quả từng dòng với kho ba tài liệu này.
Bài 8.3 — Ôn đóng gói (10 phút)
Mục tiêu 10 phút10:00Chưa bắt đầu
Không viết file mới, chỉ soi Document và Reader của chính bạn:
- Còn thuộc tính nào để
publickhông? Đưa hết vềprivate. setYearcủa bạn có kiểm tra không? Gọi thửsetYear(3000)rồisetCopies(-5). Nếu chương trình nhận bừa thì setter của bạn chỉ là phép gán, chưa phải đóng gói.Readerphải có hàm lấy họ têngetFullName()và hàm hạn mứcmaxBorrow(). Thiếu thì thêm ngay trong giờ này — tuần 10 gọi tới cả hai.- Mọi getter chỉ đọc đều phải có
constở cuối.
Thiếu một chữ
constlà không biên dịch được, chứ không phải chạy hơi sai. Bẫy này đã lấy điểm của nhiều bạn ở bài về nhà tuần 4, tuần 6 và tuần 7.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bài 8.3 không viết file mới, chỉ soi lại Document và Reader theo mục 5 ở trên. Kiểm tra bắt buộc: Reader phải có getFullName() và maxBorrow() — tuần 10 gọi tới cả hai, thiếu là tuần 10 không biên dịch được.
Bài 8.4 vẽ tay vào vở cũng được, không nộp. Vẽ Disc và vẽ Library sau khi đã chuyển sang vector, theo bảng ở mục 6.
Bài kiểm tra 15% là một đề riêng. Nó không dùng hệ thống thư viện, phát trên Moodle đúng giờ làm bài: lớp thứ tư 18/11, lớp thứ hai 23/11. Buổi hôm nay là buổi luyện, không lấy điểm — nhưng
vector, hàm ảo,overridevà destructor ảo ở trên thì đề nào cũng hỏi.
Bài 8.4 — Ôn UML (10 phút)
Mục tiêu 10 phút10:00Chưa bắt đầu
Mở lại sơ đồ LoanDate của tuần trước:


Nói được bốn điều: ngăn trên ghi gì, ngăn dưới ghi gì, dấu - khác dấu + ra sao, và một hàm friend thì ghi thế nào.
Rồi tự vẽ hai sơ đồ, vẽ tay vào vở cũng được: Disc vừa viết xong, và Library sau khi đã chuyển sang vector — kiểu của items bây giờ viết là vector<Document*>.
Đề thi cho sẵn sơ đồ UML rồi bắt viết code từ đó. Đọc được UML là điểm thật, không phải trang trí.
Thư mục bài làm — giữ trên máy, không nộp
text
<MSSV>_w08/
(toàn bộ file tuần 7, Library đã dùng vector)
Disc.h/.cpp
main.cpp
nhan_xet.txt (ba câu tự kiểm tra của bài 8.1)Biên dịch một lệnh như mọi tuần:
sh
g++ -std=c++17 -Wall *.cpp -o main.exeSau buổi
Giữ nguyên thư mục này — tuần 9 chép nó sang <MSSV>_w09 rồi làm tiếp. Tuần 8 không có bài về nhà. Thư mục nào chưa biên dịch được thì sửa cho chạy trước buổi sau; để dồn là mỗi tuần một nặng thêm.
Đáp án tuần này
Nộp bài xong rồi mở trang đáp án tuần 8 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.