Giao diện
Bài lab · Tuần 5
Tuần 5 — Tính đóng gói
Tuần này thêm vào hệ thống thư viện: đóng kín dữ liệu của
Document, thêm số bản còn trên kệ, và dựng lớpLibraryquản lý một mảng con trỏDocument*.
Mang gì từ tuần trước
Chép nguyên thư mục <MSSV>_w04/ thành <MSSV>_w05/ rồi sửa tiếp trên đó. Không tạo lại từ đầu.
Mục tiêu
- Hiểu vì sao để dữ liệu
publiclà nguy hiểm, vàprivate+ getter/setter giải quyết gì. - Đặt luật kiểm tra dữ liệu vào đúng một chỗ duy nhất (setter).
- Quản lý một mảng con trỏ cấp phát động và giải phóng đúng cách trong destructor.
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 khoá dữ liệu của Document lại và viết lớp Library giữ một mảng con trỏ. Ví dụ bên dưới vẫn dùng bài hát như tuần 4: lớp Song, thêm Album (đĩa nhạc trong kho) và Playlist (danh sách phát). Mọi đoạn code đều đã biên dịch và chạy thật. Đọc để nắm cách viết, rồi tự làm cho Document và Library.
1. Để public thì ai cũng sửa được, kể cả sửa sai
Lớp Song của tuần 4 để mọi thứ public. Không gì ngăn một dòng code trong main gán thời lượng âm:
cpp
#include <iostream>
#include <string>
using namespace std;
class Song {
public:
string title;
int seconds;
void show() const {
cout << title << " (" << seconds / 60 << " phut " << seconds % 60 << " giay)" << endl;
}
};
int main() {
Song s;
s.title = "Hello Viet Nam";
s.seconds = -50; // khong ai ngan duoc
s.show();
return 0;
}text
Hello Viet Nam (0 phut -50 giay)Chương trình biên dịch không một cảnh báo và vẫn chạy. Người viết lớp biết thời lượng phải lớn hơn 0, nhưng khi thuộc tính là public thì không có cách nào bắt mọi chỗ khác trong chương trình tuân theo. Dữ liệu hỏng ở main nhưng lộ ra ở show(); chương trình càng lớn, bạn càng dễ đi tìm lỗi sai chỗ.
2. private: g++ chặn ngay lúc biên dịch
Đưa thuộc tính xuống dưới private:. Thuộc tính private chỉ dùng được bên trong lớp, tức là trong constructor và các phương thức của chính lớp đó. main không gán được nữa, nên lớp cần constructor để nhận giá trị ban đầu:
cpp
#include <iostream>
#include <string>
using namespace std;
class Song {
private:
string title;
int seconds;
public:
Song(const string& title, int seconds) : title(title), seconds(seconds) {}
void show() const {
cout << title << " (" << seconds / 60 << " phut " << seconds % 60 << " giay)" << endl;
}
};
int main() {
Song s("Hello Viet Nam", 263);
s.seconds = -50;
s.show();
return 0;
}text
main.cpp: In function 'int main()':
main.cpp:20:7: error: 'int Song::seconds' is private within this context
20 | s.seconds = -50;
| ^~~~~~~
main.cpp:8:9: note: declared private here
8 | int seconds;
| ^~~~~~~- Dòng
errorchỉ đúng chỗ vi phạm: dòng 20 nằm trongmain, tức là bên ngoài lớp. - Dòng
note: declared private herechỉ chỗ thuộc tính được khai báo. - Constructor và
show()vẫn dùngtitle,secondsbình thường, vì chúng nằm bên trong lớp. - Xoá dòng
s.seconds = -50;thì chương trình biên dịch lại được.privatechỉ báo lỗi ở những dòng chạm thẳng vào thuộc tính từ bên ngoài; chỗ nào chỉ gọi constructor và phương thức thì không bị ảnh hưởng.
3. Getter và setter có kiểm tra
Muốn bên ngoài đọc được thì thêm getter. Muốn bên ngoài sửa được thì thêm setter, và setter là nơi kiểm tra dữ liệu trước khi nhận. Sơ đồ của Song sau khi đóng gói:


| Thấy trên UML | Viết trong file .h |
|---|---|
Dấu - ở đầu dòng | đặt dòng đó dưới private: |
+ getTitle() : string | std::string getTitle() const; (getter chỉ đọc nên có const) |
+ setSeconds(int) : void | void setSeconds(int newSeconds); (setter sửa thuộc tính nên không có const) |
Viết gọn trong một file:
cpp
#include <iostream>
#include <string>
using namespace std;
class Song {
private:
string title;
int seconds;
public:
Song(const string& title, int seconds) : title(title), seconds(0) {
setSeconds(seconds); // dung lai luat kiem tra cua setter
}
string getTitle() const { return title; }
int getSeconds() const { return seconds; }
void setSeconds(int newSeconds) {
if (newSeconds <= 0) {
cout << " -> Tu choi " << newSeconds << ", thoi luong van la " << seconds << " giay" << endl;
return;
}
seconds = newSeconds;
}
void show() const {
cout << title << " (" << seconds / 60 << " phut " << seconds % 60 << " giay)" << endl;
}
};
int main() {
Song s("Hello Viet Nam", 263);
s.setSeconds(-50);
s.show();
s.setSeconds(300);
s.show();
cout << "Tua: " << s.getTitle() << ", dai " << s.getSeconds() << " giay" << endl;
return 0;
}text
-> Tu choi -50, thoi luong van la 263 giay
Hello Viet Nam (4 phut 23 giay)
Hello Viet Nam (5 phut 0 giay)
Tua: Hello Viet Nam, dai 300 giay- Setter kiểm tra trước. Giá trị sai thì báo rồi
returnngay, thuộc tính giữ nguyên giá trị cũ. Giá trị đúng mới gán. - Constructor gọi lại
setSecondsthay vì tự kiểm tra thêm lần nữa, nên luật "thời lượng phải dương" chỉ nằm ở một chỗ. Danh sách khởi tạo đặtseconds(0)trước, để lỡ giá trị truyền vào bị từ chối thì thời lượng là 0 chứ không phải giá trị rác. - Không phải thuộc tính nào cũng cần setter.
Songkhông cósetTitle, vì tựa bài hát không đổi sau khi tạo.
4. Getter phải có const
Chữ const sau getter không chỉ để cho đẹp. Một hàm nhận const Song& chỉ được gọi những phương thức có const. Quên const ở getTitle:
cpp
#include <iostream>
#include <string>
using namespace std;
class Song {
private:
string title;
public:
Song(const string& title) : title(title) {}
string getTitle() { return title; } // quen const
};
void inTua(const Song& s) {
cout << s.getTitle() << endl;
}
int main() {
Song s("Hello Viet Nam");
inTua(s);
return 0;
}text
main.cpp: In function 'void inTua(const Song&)':
main.cpp:15:23: error: passing 'const Song' as 'this' argument discards qualifiers [-fpermissive]
15 | cout << s.getTitle() << endl;
| ~~~~~~~~~~^~
main.cpp:11:12: note: in call to 'std::string Song::getTitle()'
11 | string getTitle() { return title; } // quen const
| ^~~~~~~~discards qualifiers nghĩa là lời gọi này sẽ làm mất chữ const của s. g++ không cho, vì getTitle() không hứa là chỉ đọc. Thêm const sau getTitle() là hết lỗi. Thang điểm tuần này chấm riêng việc getter có const.
5. Hàm nghiệp vụ, và khi nào phải viết this->
Nhiều thuộc tính không nên có setter kiểu "gán gì cũng được". Số đĩa trong kho chỉ thay đổi khi bán hoặc nhập thêm, nên lớp Album chỉ cho đúng hai thao tác đó:
cpp
#include <iostream>
#include <string>
using namespace std;
class Album {
private:
string name;
int stock; // so dia con trong kho
public:
Album(const string& name, int stock) : name(name), stock(stock) {
if (stock < 0) this->stock = 0; // "stock" o day la tham so; this->stock moi la thuoc tinh
}
bool sell(int n) { // ban n dia neu du hang
if (n <= 0 || n > stock) return false;
stock -= n;
return true;
}
void restock(int n) {
if (n > 0) stock += n;
}
void show() const {
cout << name << ": con " << stock << " dia" << endl;
}
};
int main() {
Album a("Mua xuan tren thanh pho", 5);
cout << (a.sell(3) ? "Ban duoc 3 dia" : "Khong du hang") << endl;
cout << (a.sell(4) ? "Ban duoc 4 dia" : "Khong du hang") << endl;
a.restock(10);
a.show();
Album b("Dia loi", -7);
b.show();
return 0;
}text
Ban duoc 3 dia
Khong du hang
Mua xuan tren thanh pho: con 12 dia
Dia loi: con 0 diasellkiểm tra trước; không đủ hàng thì trảfalsevà không trừ. Người gọi dựa vào giá trị trả về để in thông báo phù hợp.- Không có
setStock, nên từ bên ngoài lớp không có cách nào làm số đĩa âm.
Trong constructor, tham số stock trùng tên với thuộc tính stock. Trong thân hàm, chữ stock là tham số; muốn nói tới thuộc tính phải viết this->stock. Quên this->:
cpp
if (stock < 0) stock = 0; // SAI: sua tham so, thuoc tinh van amtext
Ban duoc 3 dia
Khong du hang
Mua xuan tren thanh pho: con 12 dia
Dia loi: con -7 diag++ không cảnh báo gì, vì gán cho một tham số int là việc hợp lệ. (Tuần 4, tham số là const string& nên câu title = title; bị báo lỗi; ở đây thì im lặng.) Trong danh sách khởi tạo thì không cần this->: với stock(stock), chữ ngoài ngoặc luôn là thuộc tính.
6. Mảng con trỏ cấp phát động
Playlist giữ nhiều bài hát, nhưng số chỗ tối đa chỉ biết lúc tạo danh sách. Vì vậy constructor xin một mảng con trỏ bằng new:


Song** songs;là một con trỏ tới ô đầu tiên của mảng; mỗi ô chứa mộtSong*.songs = new Song*[capacity];xincapacityô con trỏ, chưa có bài hát nào. Ô chưa gán chứa giá trị rác, nên chỉ dùng các ô từ0tớicount - 1.- Mỗi bài hát được tạo riêng bằng
new Song(...), rồi đưa địa chỉ choadd.
cpp
#include <iostream>
#include <string>
using namespace std;
class Song {
private:
string title;
int seconds;
public:
Song(const string& title, int seconds) : title(title), seconds(seconds) {}
string getTitle() const { return title; }
void show() const {
cout << " " << title << " (" << seconds / 60 << " phut " << seconds % 60 << " giay)" << endl;
}
};
class Playlist {
private:
string name;
Song** songs; // mang cac con tro Song*
int count;
int capacity;
public:
Playlist(const string& name, int capacity) : name(name), count(0), capacity(capacity) {
songs = new Song*[capacity]; // xin cho chua 'capacity' con tro
}
~Playlist() {
for (int i = 0; i < count; i++)
delete songs[i]; // tra tung bai hat
delete[] songs; // roi tra mang con tro
cout << "(Da xoa danh sach " << name << ")" << endl;
}
bool add(Song* s) {
if (count >= capacity) return false;
songs[count] = s;
count++;
return true;
}
Song* findByTitle(const string& title) const {
for (int i = 0; i < count; i++)
if (songs[i]->getTitle() == title)
return songs[i];
return nullptr;
}
void listAll() const {
cout << "Danh sach " << name << ":" << endl;
for (int i = 0; i < count; i++)
songs[i]->show(); // songs[i] la con tro nen dung ->
}
};
int main() {
Playlist p("Di hoc", 2);
p.add(new Song("Hello Viet Nam", 263));
p.add(new Song("Mua he", 215));
Song* thua = new Song("Bai thu ba", 200);
if (!p.add(thua)) {
cout << "Danh sach day, khong them duoc: " << thua->getTitle() << endl;
delete thua; // khong vao danh sach thi tu tra bo nho
}
p.listAll();
Song* s = p.findByTitle("Mua he");
if (s != nullptr) cout << "Tim thay: " << s->getTitle() << endl;
if (p.findByTitle("Khong co") == nullptr) cout << "Khong tim thay bai nay" << endl;
return 0;
}text
Danh sach day, khong them duoc: Bai thu ba
Danh sach Di hoc:
Hello Viet Nam (4 phut 23 giay)
Mua he (3 phut 35 giay)
Tim thay: Mua he
Khong tim thay bai nay
(Da xoa danh sach Di hoc)songs[i]là mộtSong*, nên gọi phương thức bằng mũi tên:songs[i]->show().findByTitleduyệt hết mảng; không thấy thì trảnullptrở sau vòng lặp. Người gọi phải kiểm tranullptrtrước khi dùng kết quả.addthành công thì từ đóPlaylistchịu trách nhiệm trả bộ nhớ của bài hát.addthất bại thì bài hát không nằm trong danh sách, nênmainphải tựdelete thua;.
7. Destructor: trả lại đúng thứ đã xin
Destructor tên là dấu ~ cộng tên lớp, không tham số, không kiểu trả về, và tự chạy khi đối tượng bị huỷ. p là biến thường trong main, nên destructor của nó chạy lúc main kết thúc; dòng (Da xoa danh sach Di hoc) là bằng chứng.
Mỗi lần new phải có đúng một lần trả, và trả đúng kiểu:
| Xin bằng | Trả bằng | Trong Playlist |
|---|---|---|
new Song(...) (một đối tượng) | delete | delete songs[i]; cho từng bài |
new Song*[capacity] (một mảng) | delete[] | delete[] songs; |
Thứ tự quan trọng: trả từng bài hát trước, trả mảng sau. Trả mảng trước thì mất luôn các con trỏ nằm trong đó, không còn cách nào trả các bài hát.
Hai lỗi sau không có triệu chứng gì khi chạy, nên phải tự đọc code để bắt:
- Viết
delete songs;thay chodelete[] songs;. g++ không cảnh báo và chương trình in ra y hệt, nhưng theo chuẩn C++ đây là lỗi: kết quả không xác định, có thể hỏng ở máy khác hoặc lần chạy khác. - Quên hẳn destructor. Chương trình vẫn chạy, chỉ thiếu dòng thông báo, và bộ nhớ của mọi bài hát không được trả lại.
8. Ba lỗi hay gặp với con trỏ
Dùng dấu chấm thay cho mũi tên. Viết songs[i].show() thì g++ báo một dòng rất dài, nhưng cuối dòng đã gợi ý cách sửa:
text
main.cpp: In member function 'void Playlist::listAll() const':
main.cpp:54:22: error: request for member 'show' in '*(((Song**)((const Playlist*)this)->Playlist::songs) + ((sizetype)(((long long unsigned int)i) * 8)))', which is of pointer type 'Song*' (maybe you meant to use '->' ?)
54 | songs[i].show();
| ^~~~Dùng kết quả tìm kiếm mà không kiểm tra nullptr. Thay dòng tìm "Khong co" trong main bằng:
cpp
p.findByTitle("Khong co")->show(); // SAI: khong kiem tra nullptr
cout << "Dong nay co in ra khong?" << endl;g++ không báo gì. Chạy thì chương trình dừng im lặng ngay tại dòng đó:
text
Danh sach day, khong them duoc: Bai thu ba
Danh sach Di hoc:
Hello Viet Nam (4 phut 23 giay)
Mua he (3 phut 35 giay)
Tim thay: Mua heKhông có thông báo lỗi, không có dòng Dong nay co in ra khong?, destructor cũng không kịp chạy. Trong PowerShell, gõ $LASTEXITCODE ngay sau khi chạy sẽ thấy -1073741819, mã Windows cho "truy cập vùng nhớ không hợp lệ". Gặp chương trình dừng giữa chừng kiểu này, hãy nhìn dòng cuối cùng in ra được, rồi tìm con trỏ được dùng ngay sau đó.
Quên return nullptr; ở cuối hàm trả con trỏ. Lần này g++ có cảnh báo, và đừng bỏ qua nó:
text
main.cpp: In member function 'Song* Playlist::findByTitle(const std::string&) const':
main.cpp:48:5: warning: control reaches end of non-void function [-Wreturn-type]
48 | }
| ^Chạy thì chương trình cũng dừng im lặng, đúng lúc tìm một bài không có trong danh sách.
Bài 5.1 — Đóng kín lớp Document (40 phút)
Mục tiêu 40 phút40:00Chưa bắt đầu
Nhiệm vụ:
- Chuyển cả ba thuộc tính của
Documenttừpublicsangprivate, rồi biên dịch lại. Nếumain.cpptuần 4 chỉ gọi constructor vàdisplay()như đề yêu cầu thì vẫn qua. Thêm tạm vàomainmột dòng gán thẳng thuộc tính, ví dụd1.year = 1800;, rồi biên dịch lần nữa: lần này g++ báo lỗi. Đọc kỹ thông báo lỗi đó, nó nói đúng điều tính đóng gói làm được. Đọc xong thì xoá dòng thử. - Thêm thuộc tính
int copies— số bản còn trên kệ. Constructor có tham số nhận thêm số bản:Document(id, title, year, copies); constructor mặc định đặtcopies = 0. - Thêm các getter (chỉ đọc, phải có
const):getId(),getTitle(),getYear(),getCopies(). - Thêm các setter có kiểm tra:
setTitle(newTitle)— nếu chuỗi rỗng thì in cảnh báo và giữ nguyên giá trị cũ.setYear(newYear)— nếu ngoài khoảng[1900, 2026]thì in cảnh báo và giữ nguyên.
- Thêm ba hàm nghiệp vụ:
bool available() const— còn bản nào không.bool borrow()— nếu còn thì giảm 1 và trảtrue, hết thì trảfalse.void giveBack()— tăng 1.
Câu hỏi phải trả lời được khi giảng viên hỏi: nếu
copiesvẫn đểpublicthì ai có thể làm hỏng dữ liệu, và hỏng kiểu gì?
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>_w04 thành <MSSV>_w05. Tải khung bài làm tuần 5 rồi chép Library.h và Library.cpp vào thư mục mới. Chưa chép main.cpp: bước 2 và bước 3 cần main.cpp cũ.
Bước 2. Trong Document.h, thêm private: ngay trên ba thuộc tính và public: ngay trên constructor đầu tiên. Biên dịch bốn file như lệnh trong HUONG_DAN.txt. Nếu main.cpp tuần 4 chỉ gọi constructor và display() thì vẫn qua (mục 2).
Bước 3. Thêm tạm dòng d1.year = 1800; vào main rồi biên dịch lại. g++ báo (số dòng tuỳ file của bạn):
text
main.cpp: In function 'int main()':
main.cpp:10:8: error: 'int Document::year' is private within this context
10 | d1.year = 1800;
| ^~~~
In file included from main.cpp:1:
Document.h:11:9: note: declared private here
11 | int year; // nam xuat ban
| ^~~~Ghi nhớ điều thông báo này nói, nó giúp bạn trả lời câu hỏi cuối Bài 5.1. Xoá dòng thử, rồi chép main.cpp của khung đè lên main.cpp cũ.
Bước 4. Thêm int copies; dưới private:. Constructor có tham số nhận thêm copies ở cả .h lẫn .cpp, và danh sách khởi tạo có thêm copies(copies). Constructor mặc định thêm copies(0).
Bước 5. Viết bốn getter. Trong .h: std::string getId() const;. Trong .cpp: string Document::getId() const { return id; }. const phải có ở cả hai nơi (mục 4).
Bước 6. Viết setYear theo khuôn setSeconds ở mục 3: năm ngoài [1900, 2026] thì in cảnh báo rồi return, còn lại mới gán. Câu cảnh báo phải khớp kết quả mong đợi từng ký tự: hai dấu cách, dấu !, rồi câu có cả năm mới lẫn năm cũ. Câu đó in trước khi return, nên lúc in year vẫn là năm cũ. Viết setTitle tương tự, dùng newTitle.empty() để nhận ra chuỗi rỗng; câu cảnh báo của setTitle tuỳ bạn.
Bước 7. Viết available, borrow, giveBack. borrow theo khuôn sell ở mục 5: hết bản thì trả false và không trừ; còn bản thì trừ 1 rồi trả true.
Bước 8. Sửa display để in thêm | Con <số bản> ban vào cuối dòng, như kết quả mong đợi.
Bước 9. Thử riêng Document trước khi sang Library. Tạm viết vào main của khung:
cpp
Document d("TL002", "Vat ly dai cuong 1", 2021, 1);
d.setYear(2030);
d.setTitle("");
cout << (d.borrow() ? "Muon duoc" : "Het ban") << endl;
cout << (d.borrow() ? "Muon duoc" : "Het ban") << endl;
d.giveBack();
d.display();Biên dịch bốn file rồi chạy. Document đúng thì in ra như sau (dòng thứ hai là câu cảnh báo của đáp án, của bạn có thể khác):
text
! Nam 2030 khong hop le. Giu nguyen 2021.
! Tua de khong duoc de trong. Giu nguyen gia tri cu.
Muon duoc
Het ban
[TL] TL002 | Vat ly dai cuong 1 | 2021 | Con 1 banXoá các dòng thử trước khi sang Bài 5.2.
Nếu gặp no matching function for call to 'Document::Document(const char [6], const char [26], int)': main.cpp vẫn tạo Document bằng ba tham số trong khi constructor đã cần bốn (trích):
text
main.cpp: In function 'int main()':
main.cpp:7:59: error: no matching function for call to 'Document::Document(const char [6], const char [26], int)'
7 | Document d1("TL001", "Lap trinh huong doi tuong", 2019);
| ^
main.cpp:7:59: note: there are 4 candidates
In file included from main.cpp:1:
Document.h:17:5: note: candidate 1: 'Document::Document(const std::string&, const std::string&, int, int)'
17 | Document(const std::string& id, const std::string& title, int year, int copies);
| ^~~~~~~~
Document.h:17:5: note: candidate expects 4 arguments, 3 providedDòng đáng đọc là candidate expects 4 arguments, 3 provided. Các "candidate" nhận const Document& hay Document&& là constructor sao chép do C++ tự sinh, bỏ qua chúng. Dùng main.cpp của khung như bước 3 thì không gặp lỗi này.
Nếu gặp no declaration matches nhắc tới một getter: thiếu const ở .cpp hoặc ở .h, giống lỗi của display ở tuần 4.
Bài 5.2 — Lớp Library giữ mảng con trỏ (60 phút)
Mục tiêu 60 phút60:00Chưa bắt đầu


Nhiệm vụ:
- Constructor cấp phát
items = new Document*[capacity]; ~Library()phảideletetừng tài liệu rồi mớidelete[] items. In một dòng để thấy destructor có chạy thật.add— nếu đầy thì inKho da day, khong nhap them duoc.và trảfalse.findById— duyệt mảng, không thấy thì trảnullptr.lend(id)xử lý đúng ba tình huống:- không có mã đó →
Khong tim thay ma <id>! - có nhưng hết bản →
Het ban: <tua de> - còn bản →
Da cho muon: <tua de> (con <n> ban)
- không có mã đó →
receive(id)— nhận trả, tăng số bản.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Mở Library.h của khung, dịch sơ đồ theo cách tuần 4. Ba dòng mới so với tuần trước:
| Thấy trên UML | Viết trong Library.h |
|---|---|
- items : Document** | Document** items; dưới private: |
+ ~Library() | ~Library(); (destructor: không tham số, không kiểu trả về) |
+ findById(string) : Document* | Document* findById(const std::string& id) const; |
Bước 2. Constructor: danh sách khởi tạo cho name, address, count(0) và capacity; trong thân hàm là items = new Document*[capacity]; (mục 6).
Bước 3. Destructor theo khuôn ~Playlist ở mục 7: vòng lặp tới count để delete items[i];, sau vòng lặp là delete[] items;, cuối cùng in dòng (Da dong cua thu vien <tên>, giai phong bo nho). Vòng lặp dừng ở count, không phải capacity: các ô từ count trở đi chưa bao giờ được gán. Lúc soạn bài, bản duyệt tới capacity chạy thử vẫn không báo gì vì các ô đó tình cờ bằng 0, nhưng đừng trông vào may mắn đó.
Bước 4. add: kiểm tra đầy trước khi gán. count >= capacity thì in Kho da day, khong nhap them duoc. rồi trả false; còn chỗ thì gán vào ô items[count], tăng count, trả true.
Bước 5. findById: duyệt i từ 0 tới count - 1, so items[i]->getId() == id, khớp thì trả items[i]. Câu return nullptr; đặt sau vòng lặp. Đặt nó vào else của if thì hàm trả nullptr ngay khi tài liệu đầu tiên không khớp. g++ có cảnh báo cho trường hợp đó:
text
Library.cpp: In member function 'Document* Library::findById(const std::string&) const':
Library.cpp:34:1: warning: control reaches end of non-void function [-Wreturn-type]
34 | }
| ^Viết items[i]->id thay cho items[i]->getId() thì bị chặn, vì đối với Document, Library cũng là bên ngoài:
text
Library.cpp: In member function 'Document* Library::findById(const std::string&) const':
Library.cpp:29:23: error: 'std::string Document::id' is private within this context
29 | if (items[i]->id == id)
| ^~Bước 6. lend: gọi findById một lần, cất vào Document* d. Kiểm tra d == nullptr trước tiên: đúng thì in Khong tim thay ma <id>! rồi return (mục 8). Sau đó if (d->borrow()) thì in Da cho muon: ..., còn không thì in Het ban: .... Gọi d->getCopies() sau borrow() để in số bản đã giảm.
Bước 7. receive: giống lend nhưng gọi giveBack() và in Da nhan tra: <tựa đề> (con <n> ban).
Bước 8. listAll: in dòng tiêu đề --- <tên> - <địa chỉ> ---, rồi gọi items[i]->display() cho từng tài liệu.
Bước 9. Viết main.cpp theo các TODO của khung. Tạo tài liệu bằng new ngay trong lời gọi:
cpp
lib.add(new Document("TL001", "Lap trinh huong doi tuong", 2019, 2));Tựa và năm của TL002, TL003 lấy từ kết quả mong đợi. Để sửa năm của TL002, lấy con trỏ bằng Document* d = lib.findById("TL002"); rồi gọi d->setYear(...) và d->display().
Đừng đưa địa chỉ của một biến thường vào thư viện:
cpp
Document d1("TL001", "Lap trinh huong doi tuong", 2019, 2);
lib.add(&d1); // SAIChương trình in đủ mọi dòng rồi dừng ngay trong destructor, mất dòng cuối: destructor delete một đối tượng không được tạo bằng new.
Bước 10. Biên dịch bốn file, so 18 dòng với kết quả mong đợi. Dòng cuối là bằng chứng destructor đã chạy.
Nếu chương trình dừng giữa chừng mà không báo lỗi gì, nhìn dòng cuối cùng in ra được:
| Dòng cuối in ra được | Thường do |
|---|---|
Het ban: Lap trinh huong doi tuong | lend không kiểm tra nullptr (bước 6), hoặc findById thiếu return nullptr; (lúc biên dịch có cảnh báo control reaches end of non-void function) |
--- THU SUA DU LIEU --- | findById đặt return nullptr; trong else (bước 5) |
Dòng TL003 của lần listAll() thứ hai, thiếu dòng (Da dong cua ...) | Không có destructor, destructor không in gì, hoặc add nhận địa chỉ biến thường (bước 9) |
Nếu gặp request for member 'display' in ... which is of pointer type 'Document*' (maybe you meant to use '->' ?): items[i] là con trỏ, dùng -> (mục 8).
Nếu gặp undefined reference nhắc tới Library::: lệnh g++ đang thiếu Library.cpp.
Kịch bản phải chạy trong main.cpp
- Tạo thư viện
"Thu vien Khoa", địa chỉ"Nha I - Lau 1", sức chứa 5. - Thêm 3 tài liệu:
TL001(2 bản),TL002(1 bản),TL003(3 bản). listAll().- Mượn
TL001ba lần, mượnTL999một lần, trảTL001một lần. - Gọi
setYear(2030)rồisetYear(2020)trênTL002, in lại. listAll()lần nữa.
Kết quả mong đợi
text
--- Thu vien Khoa - Nha I - Lau 1 ---
[TL] TL001 | Lap trinh huong doi tuong | 2019 | Con 2 ban
[TL] TL002 | Vat ly dai cuong 1 | 2021 | Con 1 ban
[TL] TL003 | Giai tich 2 | 2018 | Con 3 ban
--- MUON TRA ---
Da cho muon: Lap trinh huong doi tuong (con 1 ban)
Da cho muon: Lap trinh huong doi tuong (con 0 ban)
Het ban: Lap trinh huong doi tuong
Khong tim thay ma TL999!
Da nhan tra: Lap trinh huong doi tuong (con 1 ban)
--- THU SUA DU LIEU ---
! Nam 2030 khong hop le. Giu nguyen 2021.
[TL] TL002 | Vat ly dai cuong 1 | 2020 | Con 1 ban
--- Thu vien Khoa - Nha I - Lau 1 ---
[TL] TL001 | Lap trinh huong doi tuong | 2019 | Con 1 ban
[TL] TL002 | Vat ly dai cuong 1 | 2020 | Con 1 ban
[TL] TL003 | Giai tich 2 | 2018 | Con 3 ban
(Da dong cua thu vien Thu vien Khoa, giai phong bo nho)Dòng cuối cùng là bằng chứng destructor đã chạy. Nếu bạn không thấy nó, bộ nhớ đang bị rò.
Thư mục bài làm — giữ trên máy, không nộp
text
<MSSV>_w05/
Document.h Document.cpp
Library.h Library.cpp
Reader.h Reader.cpp (giữ nguyên từ tuần 4, vẫn phải biên dịch được)
main.cppBài tại lớp không nộp. Tuần 5 chỉ nộp bài về nhà ở cuối trang — và bài đó được chấm lấy 15% điểm môn, nên làm bài 5.1, 5.2 tại lớp cho chắc rồi mới làm tiế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; điểm của tuần 5 nằm ở bài về nhà bên dưới.
| Nội dung | Điểm |
|---|---|
| Chương trình chạy đúng kịch bản | 3 |
Thuộc tính private, đủ getter có const | 2 |
| Setter có kiểm tra và từ chối đúng giá trị sai | 2 |
Library cấp phát và giải phóng đúng (có destructor) | 2 |
| Chia file đúng, biên dịch một lệnh | 1 |
Bài về nhà — chấm điểm (15% điểm môn)
Bài này được chấm điểm. Làm tiếp trên Library của Bài 5.2, rồi nộp trong hoạt động Phòng lập trình ảo trên Moodle (xem mục Nộp bài cuối trang để biết hạn của lớp bạn).
Chép thư mục <MSSV>_w05 thành <MSSV>_w05_bvn rồi làm trong thư mục mới. Giữ nguyên tên lớp, tên file và các hàm đã có; chỉ thêm và sửa đúng những chỗ nêu dưới đây.


Yêu cầu
- Gỡ tài liệu:
bool remove(const std::string& id).- Có mã
id:deletetài liệu đó, dồn các con trỏ phía sau lên một ô để mảng không thủng lỗ và giữ nguyên thứ tự, giảmcount, inDa go: <tua de>, trảtrue. - Không có mã
id: inKhong tim thay ma <id>!, trảfalse, không thay đổi gì. - Trong kết quả mẫu bên dưới có dòng
(remove tra ve false). Dòng đó domainin khi thấyremovetrảfalse, không phải việc củaremove— bạn mà in thêm dòng đó trongremovelà sai.
- Có mã
- Tự mở rộng kho:
addkhông từ chối khi kho đầy nữa.- Hàm
privatevoid grow(): xin mảng con trỏ mới có sức chứa gấp đôi, chép các con trỏ đang có sang,delete[]mảng cũ, cập nhậtcapacity, rồi in(Mo rong kho: <sức chứa cũ> -> <sức chứa mới> cho). addgọigrow()khicountđã bằngcapacity, rồi thêm tài liệu như cũ và luôn trảtrue.- Đề bảo đảm sức chứa truyền vào constructor luôn từ 1 trở lên, bạn không phải lo trường hợp sức chứa 0.
- Hàm
- Destructor,
findById,lend,receive,listAllgiữ nguyên như Bài 5.2.
Máy chấm gọi đúng những hàm này
main.cpp của giảng viên chỉ gọi đúng những hàm dưới đây. Tên hàm, thứ tự tham số phải khớp, và tất cả phải nằm trong phần public — riêng grow thì private:
cpp
Library lib("Thu vien Khoa", "Nha I - Lau 1", 5); // constructor ba tham số
lib.add(new Document("TL001", "Lap trinh huong doi tuong", 2019, 2)); // Document bốn tham số
lib.remove("TL001");
lib.lend("TL001");
lib.receive("TL001");
lib.listAll();
Document* d = lib.findById("TL001"); // phải public, trả về Document*
d->setYear(2005);
d->display();Đổi tên hàm, đổi thứ tự tham số, hay để findById trong private thì máy không biên dịch được, và bài không biên dịch được thì 0 điểm — không có điểm từng phần.
Kịch bản
Khung bài làm tuần 5 có bai_ve_nha/main.cpp. Chép đè file đó lên main.cpp trong thư mục <MSSV>_w05_bvn, không sửa dòng nào. File này chỉ để bạn chạy thử ở máy mình — lúc nộp thì không nộp nó. Kết quả mong đợi:
text
--- NHAP KHO ---
(Mo rong kho: 2 -> 4 cho)
(Mo rong kho: 4 -> 8 cho)
--- Thu vien Khoa - Nha I - Lau 1 ---
[TL] TL001 | Lap trinh huong doi tuong | 2019 | Con 2 ban
[TL] TL002 | Vat ly dai cuong 1 | 2021 | Con 1 ban
[TL] TL003 | Giai tich 2 | 2018 | Con 3 ban
[TL] TL004 | Hoa hoc dai cuong | 2020 | Con 1 ban
[TL] TL005 | Xac suat thong ke | 2022 | Con 2 ban
--- GO TAI LIEU ---
Da go: Vat ly dai cuong 1
Da go: Lap trinh huong doi tuong
Da go: Xac suat thong ke
Khong tim thay ma TL999!
(remove tra ve false)
--- Thu vien Khoa - Nha I - Lau 1 ---
[TL] TL003 | Giai tich 2 | 2018 | Con 3 ban
[TL] TL004 | Hoa hoc dai cuong | 2020 | Con 1 ban
--- MUON SAU KHI GO ---
Da cho muon: Hoa hoc dai cuong (con 0 ban)
Khong tim thay ma TL002!
--- Thu vien Khoa - Nha I - Lau 1 ---
[TL] TL003 | Giai tich 2 | 2018 | Con 3 ban
[TL] TL004 | Hoa hoc dai cuong | 2020 | Con 0 ban
(Da dong cua thu vien Thu vien Khoa, giai phong bo nho)Chấm điểm
Máy ghép bốn file của bạn với một main.cpp của giảng viên — file này không công bố — rồi chạy bốn bộ kiểm thử: kịch bản y như trên; kho chỉ 1 chỗ nên phải mở rộng hai lần, gỡ hết rồi thêm lại; gỡ ở cuối rồi gỡ ở đầu để soát thứ tự; và một bộ chỉ đụng tới getter, setter và mượn trả. Mỗi bộ 2,5 điểm, sai một dòng là mất trọn bộ đó. Vì vậy code phải đúng thật, không chỉ in vừa khớp kịch bản ở trên.
| Bộ kiểm thử | Điểm |
|---|---|
| Bộ 1 — kịch bản như đề | 2,5 |
| Bộ 2 — kho 1 chỗ, mở rộng hai lần, gỡ hết rồi thêm lại | 2,5 |
| Bộ 3 — gỡ ở cuối rồi gỡ ở đầu, thứ tự phải giữ nguyên | 2,5 |
| Bộ 4 — getter, setter có kiểm tra, mượn tới hết bản | 2,5 |
Máy không nhìn được bộ nhớ: remove quên delete, grow quên delete[] thì chương trình vẫn in đúng. Giảng viên đọc code và trừ tới 2 điểm nếu thiếu hai chỗ đó, nên đừng bỏ.
Nộp sau hạn thì trừ 10% điểm bài đó — ví dụ chạy đúng cả bốn bộ được 10 điể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.
Điểm 10 quy về 15% điểm môn.
Nộp bài
- Nộp đúng bốn file:
Document.h,Document.cpp,Library.h,Library.cpp. - Không nộp
main.cpp— máy đã cómainriêng để chấm, nộp thêm sẽ lỗi trùngmain. Cũng không nộpReader.*. - Trong hoạt động Phòng lập trình ảo, 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. Trong bốn tệp đó đừng#include "Reader.h", vì máy chấm không có file đó. - 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 6 của lớp bạn: lớp học thứ hai — 19:00 chủ nhật 18/10; lớp học thứ tư — 19:00 thứ ba 20/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 5 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.