Skip to content

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: Library bỏ mảng con trỏ tự quản lý, chuyển sang std::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 vector hoặc thiếu Disc thì 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 .h và .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 .h và 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
  9

Ba việc bạn cần, chỉ ba thôi:

ViệcViết
Khai báovector<Kiểu> tên;
Thêm vào cuốitên.push_back(giá_trị);
Duyệt hếtfor (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ết for (int i = 0; i < ds.size(); i++) thì g++ cảnh báo:

text
warning: comparison of integer expressions of different signedness: 'int' and
'std::vector&lt;Track*>::size_type' {aka 'long long unsigned int'} [-Wsign-compare]

Để so được, g++ phải đổi int sang không dấu — và một int âm khi đó biến thành một số rất lớn. Dùng for (Track* t : ds) là hết chuyện; thật sự cần chỉ số thì khai size_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 giay

Cù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ứa vector<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 ban
text
[BAI HAT] Dieu con mai | Hong Nhung | 245 giay
[PODCAST] Vat ly moi ngay | dan: Minh Tri | 1800 giay
[BAN GHI] Tieng mua | 120 giay

Quy 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 delete thì 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;
};

#endif
cpp
#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. virtual và override chỉ viết ở .h, chép sang .cpp là lỗi biên dịch. Tên hàm trong .cpp phả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à .h nà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 dong

Ba 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 tra
cpp
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 giay

Getter 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 UMLViết trong file .h
- items : vector<Document*>std::vector<Document*> items;
+ add(Document*) : voidvoid add(Document* d);
+ {virtual} display() : voidvirtual void display() const;
+ display() : void {override} ở lớp convoid 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 da

Ba 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ướcSau
ConstructorLibrary(name, address, capacity)Library(const std::string& name) — bỏ cả address lẫn capacity
addbool add(Document* d)void add(Document* d), thân hàm là items.push_back(d);
Duyệtfor (int i = 0; i < count; i++)for (Document* d : items)
lendvoid lend(const string& id)bool lend(const string& id) — trả true nếu mượn được
Destructordelete items[i] rồi delete[] itemsfor (Document* d : items) delete d; — không còn delete[]

vector dọn chỗ chứa con trỏ, nó không dọn đối tượng mà con trỏ trỏ tới. Bỏ delete trong 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 ​

  1. items.size() trả về kiểu gì? Vì sao for (int i = 0; i < items.size(); i++) bị g++ cảnh báo?
  2. vector tự dọn, vậy vì sao destructor vẫn phải delete?
  3. 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ột Book vào kiểu vector đó rồi gọi display(), 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 đó:

  1. Thêm #include <vector> vào Library.h, đổi ba thuộc tính thành std::vector<Document*> items;.
  2. Sửa constructor trước — bỏ cả address lẫn capacity. Biên dịch: lúc này main.cpp sẽ báo lỗi ở dòng tạo Library, sửa luôn dòng đó.
  3. Sửa add thành void, thân hàm một dòng items.push_back(d);.
  4. Đổi mọi vòng for (int i = 0; i < count; i++) thành for (Document* d : items).
  5. Sửa destructor: for (Document* d : items) delete d;, xoá dòng delete[] items.
  6. Đổi lend sang 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'.
  7. Bỏ các 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: 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> ban
  • loanDays() 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 ban

D001 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ọi ds[i]->loanDays() duy nhất. Thêm Disc mà không phải sửa Document, Library hay 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 ​

  1. Disc gọi constructor của Document ở chỗ nào? Chỉ đúng dòng đó ra.
  2. Nếu bỏ chữ override ở display() thì g++ nói gì? Nếu gõ sai thành dispaly() thì sao?
  3. ~Document() của bạn có virtual không? Không có thì delete một Document* trỏ tới Disc sẽ 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 để public không? Đưa hết về private.
  • setYear của bạn có kiểm tra không? Gọi thử setYear(3000) rồi setCopies(-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.
  • Reader phải có hàm lấy họ tên getFullName() và hàm hạn mức maxBorrow(). 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ữ const là 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, override và 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:

Sơ đồ UML lớp LoanDate: ba thuộc tính private day, month, year; constructor, destructor, ba toán tử +, -, << (<< là friend) và ba hàm phụ privateSơ đồ UML lớp LoanDate: ba thuộc tính private day, month, year; constructor, destructor, ba toán tử +, -, << (<< là friend) và ba hàm phụ private

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.exe

Sau 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.

© 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