Skip to content

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ớp Library quả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 public là 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.

Tải khung bài làm tuần 5 (.zip)

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 error chỉ đúng chỗ vi phạm: dòng 20 nằm trong main, tức là bên ngoài lớp.
  • Dòng note: declared private here chỉ chỗ thuộc tính được khai báo.
  • Constructor và show() vẫn dùng title, seconds bì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. private chỉ 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:

Sơ đồ UML lớp Song đã đóng gói: hai thuộc tính private title, seconds; constructor, hai getter, một setter và hàm showSơ đồ UML lớp Song đã đóng gói: hai thuộc tính private title, seconds; constructor, hai getter, một setter và hàm show
Thấy trên UMLViết trong file .h
Dấu - ở đầu dòngđặt dòng đó dưới private:
+ getTitle() : stringstd::string getTitle() const; (getter chỉ đọc nên có const)
+ setSeconds(int) : voidvoid 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 return ngay, thuộc tính giữ nguyên giá trị cũ. Giá trị đúng mới gán.
  • Constructor gọi lại setSeconds thay 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 đặt seconds(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. Song khô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 dia
  • sell kiểm tra trước; không đủ hàng thì trả false và 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 am
text
Ban duoc 3 dia
Khong du hang
Mua xuan tren thanh pho: con 12 dia
Dia loi: con -7 dia

g++ 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:

Sơ đồ UML lớp Playlist: bốn thuộc tính private, trong đó songs là mảng con trỏ Song cấp phát động; constructor, destructor, add, findByTitle, listAllSơ đồ UML lớp Playlist: bốn thuộc tính private, trong đó songs là mảng con trỏ Song cấp phát động; constructor, destructor, add, findByTitle, listAll
  • Song** songs; là một con trỏ tới ô đầu tiên của mảng; mỗi ô chứa một Song*.
  • songs = new Song*[capacity]; xin capacity ô 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ừ 0 tới count - 1.
  • Mỗi bài hát được tạo riêng bằng new Song(...), rồi đưa địa chỉ cho add.
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ột Song*, nên gọi phương thức bằng mũi tên: songs[i]->show().
  • findByTitle duyệ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 tra nullptr trước khi dùng kết quả.
  • add thành công thì từ đó Playlist chịu trách nhiệm trả bộ nhớ của bài hát. add thất bại thì bài hát không nằm trong danh sách, nên main phả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ằngTrả bằngTrong Playlist
new Song(...) (một đối tượng)deletedelete 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 cho delete[] 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 he

Khô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ụ:

  1. Chuyển cả ba thuộc tính của Document từ public sang private, rồi biên dịch lại. Nếu main.cpp tuần 4 chỉ gọi constructor và display() như đề yêu cầu thì vẫn qua. Thêm tạm vào main mộ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ử.
  2. 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 đặt copies = 0.
  3. Thêm các getter (chỉ đọc, phải có const): getId(), getTitle(), getYear(), getCopies().
  4. 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.
  5. 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 copies vẫn để public thì 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 ban

Xoá 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 provided

Dò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
Sơ đồ UML lớp Library: năm thuộc tính private, trong đó items là mảng con trỏ Document cấp phát động; constructor, destructor và năm phương thứcSơ đồ UML lớp Library: năm thuộc tính private, trong đó items là mảng con trỏ Document cấp phát động; constructor, destructor và năm phương thức

Nhiệm vụ:

  1. Constructor cấp phát items = new Document*[capacity];
  2. ~Library() phải delete từng tài liệu rồi mới delete[] items. In một dòng để thấy destructor có chạy thật.
  3. add — nếu đầy thì in Kho da day, khong nhap them duoc. và trả false.
  4. findById — duyệt mảng, không thấy thì trả nullptr.
  5. 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)
  6. 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 UMLViế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);                      // SAI

Chươ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 đượcThường do
Het ban: Lap trinh huong doi tuonglend 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 ​

  1. Tạo thư viện "Thu vien Khoa", địa chỉ "Nha I - Lau 1", sức chứa 5.
  2. Thêm 3 tài liệu: TL001 (2 bản), TL002 (1 bản), TL003 (3 bản).
  3. listAll().
  4. Mượn TL001 ba lần, mượn TL999 một lần, trả TL001 một lần.
  5. Gọi setYear(2030) rồi setYear(2020) trên TL002, in lại.
  6. 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.cpp

Bà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ản3
Thuộc tính private, đủ getter có const2
Setter có kiểm tra và từ chối đúng giá trị sai2
Library cấp phát và giải phóng đúng (có destructor)2
Chia file đúng, biên dịch một lệnh1

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.

Sơ đồ UML lớp Library của bài về nhà: thêm phương thức public remove và phương thức private grow; add tự gọi grow khi kho đầySơ đồ UML lớp Library của bài về nhà: thêm phương thức public remove và phương thức private grow; add tự gọi grow khi kho đầy

Yêu cầu ​

  1. Gỡ tài liệu: bool remove(const std::string& id).
    • Có mã id: delete tà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ảm count, in Da go: <tua de>, trả true.
    • Không có mã id: in Khong 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 đó do main in khi thấy remove trả false, không phải việc của remove — bạn mà in thêm dòng đó trong remove là sai.
  2. Tự mở rộng kho: add không từ chối khi kho đầy nữa.
    • Hàm private void 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ật capacity, rồi in (Mo rong kho: <sức chứa cũ> -> <sức chứa mới> cho).
    • add gọi grow() khi count đã bằng capacity, 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.
  3. Destructor, findById, lend, receive, listAll giữ 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ại2,5
Bộ 3 — gỡ ở cuối rồi gỡ ở đầu, thứ tự phải giữ nguyên2,5
Bộ 4 — getter, setter có kiểm tra, mượn tới hết bản2,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ó main riêng để chấm, nộp thêm sẽ lỗi trùng main. Cũng không nộp Reader.*.
  • 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.

© 2026 Ngan V.T. Nguyen · Nội dung theo giấy phép CC BY-NC-SA 4.0 · Code ví dụ theo giấy phép MIT · Ảnh của bên thứ ba giữ giấy phép riêng, ghi ở Phụ lục E

Nội dung theo giấy phép CC BY-NC-SA 4.0 · Code ví dụ theo giấy phép MIT · Ảnh của bên thứ ba giữ giấy phép riêng, ghi ở Phụ lục E