Skip to content

Bài lab · Tuần 9

Tuần 9 — Chữa bài kiểm tra và tính trừu tượng ​

Hai phần dưới đây rời nhau và tuỳ lớp mà xếp buổi nào trước. Phần 1 chữa bài kiểm tra 15% trong 45 phút; phần 2 là nội dung mới — Document trở thành lớp trừu tượng, thêm loại tài liệu thứ tư (Thesis), và một giao diệnRenewable cho những tài liệu gia hạn được.

Phần 1 — Chữa bài kiểm tra (45 phút) ​

Mở lại thư mục <MSSV>_kiemtra — thư mục bài kiểm tra quản lý thiết bị phòng thí nghiệm, không phải thư mục <MSSV>_w08. Giảng viên chữa lần lượt theo từng mục của thang điểm; bạn sửa ngay trên bài mình, đừng gõ lại từ đầu. Ai đã nhận điểm rồi thì sửa đúng những mục bị trừ.

Sửa cho chạy được là bắt buộc, không phải để lấy lại điểm. Đề kiểm tra là một đề riêng, không dùng lại thư mục thư viện, nên phần này không cản phần 2 — nhưng kỹ thuật bị trừ điểm ở đây sẽ gặp lại nguyên vẹn trong đề thi cuối kỳ.

Phần 2 — Tính trừu tượng ​

Mang gì sang ​

Chép <MSSV>_w08/ thành <MSSV>_w09/ — thư mục thư viện của phần 1 tuần 8, có Disc và Library đã chuyển sang vector. Thư mục <MSSV>_kiemtra của bài kiểm tra không liên quan tới đây, cứ để nguyên.

Mục tiêu ​

  • Hiểu hàm thuần ảo (= 0) và lớp trừu tượng.
  • Thấy được lớp trừu tượng ép mọi lớp con phải tự khai báo phần riêng của mình.
  • Viết một giao diện (interface) và dùng đa kế thừa để "ký" giao diện đó.
  • Kiểm chứng thiết kế: thêm một loại tài liệu mới mà không sửa lớp nào có sẵn.

Khung bài làm

Tải về, giải nén rồi mở thư mục bằng công cụ bạn dùng. Trong đó có sẵn các file cần nộp với những chỗ ghi TODO để bạn tự viết, cùng file HUONG_DAN.txt ghi cách biên dịch.

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

Lý thuyết cần nắm ​

Tuần này Document trở thành lớp trừu tượng, có thêm loại tài liệu thứ tư và một giao diện Renewable. Ví dụ vẫn ở chuyện âm nhạc: Track giờ là lớp trừu tượng, thêm giao diện Karaoke cho những bản ghi hát karaoke được. Mọi đoạn code đều đã biên dịch và chạy thật.

Về việc chia .h và .cpp. Nhiều bạn phản ánh là trang lý thuyết gom cả lớp vào một đoạn code, trong khi bài thực hành lại bắt tách ra — đúng là lệch, và từ nay trang lý thuyết chia đúng như khi bạn làm bài. Riêng tuần này có thêm một điều: hàm thuần ảo = 0 không có thân hàm nên nó không xuống .cpp, còn giao diện thì không có file .cpp nào cả.

1. Hàm thuần ảo và lớp trừu tượng ​

Một bản ghi chung chung thì chưa có giá và cũng chưa biết mình thuộc loại gì; chỉ bài hát hay podcast cụ thể mới biết. Vì vậy Track chỉ khai báo hai hàm đó, không viết thân, và bắt mọi lớp con tự viết:

Sơ đồ UML: lớp trừu tượng Track với hai hàm thuần ảo price và kind; giao diện Karaoke với hai hàm thuần ảo; Podcast kế thừa Track; Song kế thừa Track và thực thi KaraokeSơ đồ UML: lớp trừu tượng Track với hai hàm thuần ảo price và kind; giao diện Karaoke với hai hàm thuần ảo; Podcast kế thừa Track; Song kế thừa Track và thực thi Karaoke
Thấy trên UMLViết trong C++
Tên lớp in nghiêng, ghi {abstract}lớp có ít nhất một hàm thuần ảo
Dòng in nghiêng + price() : int = 0virtual int price() const = 0;
{interface}lớp chỉ có hàm thuần ảo và destructor ảo, không có thuộc tính
Mũi tên nét đứt, đầu tam giác rỗng từ Song lên Karaokeclass Song : public Track, public Karaoke

Tám file, đúng cách bài thực hành bắt chia. Ba file đầu — giao diện và lớp trừu tượng: Có một điều chỉ lớp trừu tượng mới gặp: hàm thuần ảo không có thân hàm, nên nó không xuống .cpp — khai báo ở .h là xong. Giao diện thì toàn hàm thuần ảo, nên nó không có file .cpp nào cả.

cpp
#ifndef KARAOKE_H
#define KARAOKE_H

#include <string>

// GIAO DIEN: chi co ham thuan ao, khong co thuoc tinh, khong co file .cpp
class Karaoke {
public:
    virtual int lineCount() const = 0;
    virtual std::string firstLine() const = 0;
    virtual ~Karaoke() = default;
};

#endif
cpp
#ifndef TRACK_H
#define TRACK_H

#include <string>

// LOP TRUU TUONG: co ham thuan ao nen khong tao doi tuong duoc
class Track {
protected:
    std::string title;
    int seconds;

public:
    Track(const std::string& title, int seconds);
    virtual ~Track();

    virtual int price() const = 0;          // moi loai TU dinh gia
    virtual std::string kind() const = 0;   // moi loai TU khai ten loai

    std::string getTitle() const;
    void show() const;                      // ham thuong, dung hai ham thuan ao
};

#endif
cpp
#include "Track.h"
#include <iostream>
using namespace std;

Track::Track(const string& title, int seconds) {
    this->title = title;
    this->seconds = seconds;
}

Track::~Track() {}

string Track::getTitle() const { return title; }

void Track::show() const {
    cout << "  " << title << " -> loai: " << kind() << ", gia " << price() << " dong" << endl;
}

Track::show() gọi kind() và price() — hai hàm mà Track chưa hề viết. Nó vẫn biên dịch được, vì lúc chạy đối tượng thật luôn là một lớp con đã viết đủ hai hàm đó.

Năm file còn lại — hai lớp con và main:

cpp
#ifndef SONG_H
#define SONG_H

#include "Track.h"
#include "Karaoke.h"

// la mot Track, VA ky giao dien Karaoke
class Song : public Track, public Karaoke {
private:
    int lines;
    std::string first;

public:
    Song(const std::string& title, int seconds, int lines, const std::string& first);

    int price() const override;
    std::string kind() const override;
    int lineCount() const override;
    std::string firstLine() const override;
};

#endif
cpp
#include "Song.h"
using namespace std;

Song::Song(const string& title, int seconds, int lines, const string& first)
    : Track(title, seconds) {
    this->lines = lines;
    this->first = first;
}

int Song::price() const       { return 10000; }
string Song::kind() const     { return "Bai hat"; }
int Song::lineCount() const   { return lines; }
string Song::firstLine() const { return first; }
cpp
#ifndef PODCAST_H
#define PODCAST_H

#include "Track.h"

class Podcast : public Track {
private:
    int episode;

public:
    Podcast(const std::string& title, int seconds, int episode);

    int price() const override;
    std::string kind() const override;
};

#endif
cpp
#include "Podcast.h"
using namespace std;

Podcast::Podcast(const string& title, int seconds, int episode)
    : Track(title, seconds) {
    this->episode = episode;
}

int Podcast::price() const   { return 0; }
string Podcast::kind() const { return "Podcast"; }
cpp
#include "Song.h"
#include "Podcast.h"
#include <iostream>
using namespace std;

int main() {
    Track* ds[2];
    ds[0] = new Song("Hello Viet Nam", 263, 32, "Xin chao Viet Nam");
    ds[1] = new Podcast("Chuyen lap trinh", 1800, 12);

    cout << "--- MOI LOAI TU KHAI BAO ---" << endl;
    for (int i = 0; i < 2; i++)
        ds[i]->show();

    cout << "--- HAT KARAOKE DUOC KHONG? ---" << endl;
    for (int i = 0; i < 2; i++) {
        Karaoke* k = dynamic_cast<Karaoke*>(ds[i]);   // doi tuong nay co ky giao dien Karaoke khong?
        if (k == nullptr)
            cout << "  " << ds[i]->getTitle() << ": khong hat karaoke duoc." << endl;
        else
            cout << "  " << ds[i]->getTitle() << ": " << k->lineCount()
                 << " dong, dong dau: " << k->firstLine() << endl;
    }

    for (int i = 0; i < 2; i++)
        delete ds[i];
    return 0;
}

Biên dịch cả tám file bằng một lệnh — .h không bao giờ đưa vào lệnh g++, nó đã được #include rồi:

sh
g++ -std=c++17 -Wall *.cpp -o main.exe
text
--- MOI LOAI TU KHAI BAO ---
  Hello Viet Nam -> loai: Bai hat, gia 10000 dong
  Chuyen lap trinh -> loai: Podcast, gia 0 dong
--- HAT KARAOKE DUOC KHONG? ---
  Hello Viet Nam: 32 dong, dong dau: Xin chao Viet Nam
  Chuyen lap trinh: khong hat karaoke duoc.
  • = 0 biến hàm thành hàm thuần ảo: Track không viết thân cho nó, lớp con nào muốn tạo được đối tượng thì phải viết.
  • Lớp có ít nhất một hàm thuần ảo là lớp trừu tượng. Nó vẫn có thuộc tính, constructor và hàm thường như show().
  • show() không ảo nhưng gọi hai hàm thuần ảo, nên mỗi loại tự điền phần của mình vào cùng một dòng in.

2. Không tạo được đối tượng của lớp trừu tượng ​

Thử tạo một Track (trích):

text
main.cpp: In function 'int main()':
main.cpp:59:42: error: invalid new-expression of abstract class type 'Track'
   59 |     Track* t = new Track("Ban thu am", 95);
      |                                          ^
main.cpp:12:7: note:   because the following virtual functions are pure within 'Track':
   12 | class Track {                            // LOP TRUU TUONG: co ham thuan ao
      |       ^~~~~
main.cpp:21:17: note:     'virtual int Track::price() const'
   21 |     virtual int price() const = 0;       // moi loai TU dinh gia
      |                 ^~~~~
main.cpp:22:20: note:     'virtual std::string Track::kind() const'
   22 |     virtual string kind() const = 0;     // moi loai TU khai ten loai
      |                    ^~~~

Các dòng note liệt kê đúng những hàm còn thuần ảo. Tạo biến thường cũng bị chặn:

text
main.cpp: In function 'int main()':
main.cpp:59:11: error: cannot declare variable 't' to be of abstract type 'Track'
   59 |     Track t("Ban thu am", 95);

Còn con trỏ Track* thì vẫn dùng bình thường, như mảng Track* ds[2] ở trên: nó chỉ trỏ tới đối tượng của lớp con.

3. Lớp con quên viết một hàm thuần ảo ​

Lớp con đó cũng thành trừu tượng. Bỏ kind() của Podcast đi thì lỗi hiện ra ở chỗ new Podcast, không phải trong lớp Podcast; dòng note cho biết còn thiếu hàm nào:

text
main.cpp: In function 'int main()':
main.cpp:60:53: error: invalid new-expression of abstract class type 'Podcast'
   60 |     ds[1] = new Podcast("Chuyen lap trinh", 1800, 12);
      |                                                     ^
main.cpp:46:7: note:   because the following virtual functions are pure within 'Podcast':
   46 | class Podcast : public Track {
      |       ^~~~~~~
main.cpp:22:20: note:     'virtual std::string Track::kind() const'
   22 |     virtual string kind() const = 0;     // moi loai TU khai ten loai
      |                    ^~~~

Đây chính là điều lớp trừu tượng làm được: thêm một loại mới mà quên khai phần riêng thì g++ nhắc ngay.

4. Giao diện và đa kế thừa ​

Karaoke là một giao diện: chỉ có hàm thuần ảo và destructor ảo, không có dữ liệu. Nó không nói bản ghi "là gì", mà nói bản ghi "biết làm gì".

  • class Song : public Track, public Karaoke: bài hát là một bản ghi, và biết hát karaoke. Podcast không ký giao diện này.
  • dynamic_cast<Karaoke*>(ds[i]) hỏi lúc chạy: đối tượng mà ds[i] trỏ tới có ký Karaoke không? Có thì trả về con trỏ dùng được, không thì trả nullptr.
  • Luôn kiểm tra nullptr trước khi gọi hàm qua con trỏ vừa đổi.

5. Hai lỗi âm thầm với giao diện ​

Quên public trước tên giao diện. Viết class Song : public Track, Karaoke thì vẫn biên dịch không một cảnh báo, nhưng cả bài hát cũng trả lời là không hát được:

text
--- HAT KARAOKE DUOC KHONG? ---
  Hello Viet Nam: khong hat karaoke duoc.
  Chuyen lap trinh: khong hat karaoke duoc.

Thiếu public, Song kế thừa Karaoke kiểu riêng tư: bên ngoài lớp không ai thấy Song đã ký giao diện, nên dynamic_cast trả nullptr.

Không kiểm tra nullptr. Gọi thẳng k->lineCount() thì chương trình dừng im lặng khi tới podcast, như lỗi con trỏ của tuần 5:

text
--- MOI LOAI TU KHAI BAO ---
  Hello Viet Nam -> loai: Bai hat, gia 10000 dong
  Chuyen lap trinh -> loai: Podcast, gia 0 dong
--- HAT KARAOKE DUOC KHONG? ---
  Hello Viet Nam: 32 dong, dong dau: Xin chao Viet Nam

Bài 9.1 — Biến Document thành lớp trừu tượng ​

Nhiệm vụ:

  1. Trong Document.h, đổi loanDays() thành hàm thuần ảo và thêm một hàm thuần ảo nữa:

    cpp
    virtual int loanDays() const = 0;
    virtual std::string typeName() const = 0;
  2. Xoá phần cài đặt Document::loanDays() trong Document.cpp.

  3. Thêm typeName() cho cả ba lớp con: "Sach", "Tap chi", "Dia".

  4. Thử nghiệm bắt buộc. Thêm dòng này vào main rồi biên dịch:

    cpp
    Document* d = new Document("TL009", "Bao cao", 2023, 1);

    Nó sẽ không biên dịch được. Chép nguyên thông báo lỗi vào nhan_xet.txt. Bạn sẽ thấy g++ nói gần đúng như sau:

    cpp
    error: invalid new-expression of abstract class type 'Document'
    note:   because the following virtual functions are pure within 'Document':
              'virtual int Document::loanDays() const'
              'virtual std::string Document::typeName() const'

    Sau khi ghi lại, hãy comment dòng đó lại.

    Giải thích trong 2–3 dòng: vì sao ngăn không cho tạo Document lại là điều tốt cho hệ thống thư viện?

Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí

Bước 1. Nửa đầu buổi chữa bài tuần 8 ngay trong thư mục của bạn. Xong thì chép <MSSV>_w08 thành <MSSV>_w09, tải khung bài làm tuần 9 và chép năm file trong đó vào, đè lên main.cpp cũ. Biên dịch g++ -std=c++17 -Wall *.cpp -o main.exe: chưa in gì nhưng phải qua.

Bước 2. Trong Document.h, đổi loanDays() thành virtual int loanDays() const = 0; và thêm virtual std::string typeName() const = 0;. Xoá thân Document::loanDays() trong Document.cpp.

Bước 3. Thêm typeName() cho Book, Magazine, Disc: khai báo trong .h có override, thân trong .cpp trả "Sach", "Tap chi", "Dia".

Bước 4. Làm TODO 0 trong main.cpp: thêm dòng tạo Document của đề rồi biên dịch. Chép nguyên văn thông báo lỗi vào nhan_xet.txt, kể cả các dòng note liệt kê hai hàm thuần ảo, rồi comment dòng đó lại (mục 2).

Bước 5. Làm TODO 1 và TODO 2 với ba tài liệu đã có; LV001 thêm sau Bài 9.2. Biên dịch, so các dòng của phần 9.1.

Nếu gặp invalid new-expression of abstract class type 'Disc' (hoặc 'Book', 'Magazine'): lớp con đó chưa viết đủ hàm thuần ảo, hoặc viết lệch chữ ký, thường là thiếu const. Dòng note cuối cho biết hàm nào (mục 3):

text
main.cpp: In function 'int main()':
main.cpp:18:65: error: invalid new-expression of abstract class type 'Disc'
   18 |     lib.add(new Disc("D001", "Thi nghiem quang hoc", 2022, 1, 45));
      |                                                                 ^
In file included from main.cpp:4:
Disc.h:7:7: note:   because the following virtual functions are pure within 'Disc':
    7 | class Disc : public Document {
      |       ^~~~
In file included from Library.h:6,
                 from main.cpp:1:
Document.h:34:25: note:     'virtual std::string Document::typeName() const'
   34 |     virtual std::string typeName() const = 0;      // thuan ao: ten loai tai lieu
      |                         ^~~~~~~~

Nếu gặp lỗi đó với chính Document ở một dòng khác dòng thử nghiệm: còn chỗ nào đó tạo Document, ví dụ đoạn new Document(...) sót lại từ tuần 7. Xoá chỗ đó đi.


Bài 9.2 — Thêm loại tài liệu thứ tư: Thesis (luận văn) ​

  • Thuộc tính riêng: supervisor (giảng viên hướng dẫn), major (chuyên ngành).
  • loanDays() trả về 7, typeName() trả về "Luan van".
  • display() in dạng [LUAN VAN] ...

Đây là bài kiểm tra thiết kế của bạn. Nếu các tuần trước làm đúng, thêm lớp này không phải sửa một dòng nào trong Document, Library, LoanSlip hay main (ngoài dòng lib.add(new Thesis(...))). Nếu bạn buộc phải sửa các lớp cũ, hãy ghi vào nhan_xet.txt chỗ nào phải sửa và vì sao.

Ở tuần 6 bạn đã bấm giờ khi thêm lớp Newspaper. So lại: lần này nhanh hơn hay chậm hơn?

Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí

Bước 1. Làm TODO 1 và TODO 2 trong Thesis.h: kế thừa Document, hai thuộc tính supervisor, major, constructor sáu tham số, và ba hàm display, loanDays, typeName có override.

Bước 2. Trong Thesis.cpp: constructor gọi Document(...) trước; loanDays() trả 7, typeName() trả "Luan van"; display() theo dòng [LUAN VAN] của kết quả mong đợi, để ý chữ GVHD đứng trước tên giảng viên.

Bước 3. Trong main, thêm LV001 bằng lib.add(new Thesis(...)), rồi thêm mã LV001 vào vòng lặp của phần 9.1. Biên dịch, so dòng [LUAN VAN] và dòng LV001 -> loai: Luan van, han muon 7 ngay.

Bước 4. Trả lời câu 3 và 4 trong nhan_xet.txt: có phải sửa Document, Library hay LoanSlip không, và mất bao nhiêu phút so với lần thêm Newspaper ở tuần 6.


Bài 9.3 — Giao diện Renewable ​

Sách và luận văn thì gia hạn được; tạp chí và đĩa thì không. Đây không phải chuyện kế thừa — nó là chuyện "có biết làm việc này hay không". Dùng giao diện:

cpp
// Renewable.h - chi co ham thuan ao, khong co du lieu
class Renewable {
public:
    virtual bool renew(int days) = 0;
    virtual int  renewLeft() const = 0;
    virtual ~Renewable() = default;
};

Nhiệm vụ:

  1. class Book : public Document, public Renewable — sách gia hạn tối đa 2 lần.
  2. class Thesis : public Document, public Renewable — luận văn gia hạn tối đa 1 lần.
  3. Magazine và Disc không kế thừa Renewable.
  4. Trong main, duyệt bốn tài liệu và hỏi từng cái có gia hạn được không:
    cpp
    Renewable* r = dynamic_cast<Renewable*>(d);
    if (r == nullptr) { /* khong gia han duoc */ }
    else              { /* r->renewLeft(), r->renew(7) */ }
    dynamic_cast hỏi lúc chạy: đối tượng này có ký giao diện Renewable không? Trả nullptr nghĩa là không.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí

Bước 1. Renewable.h trong khung đã đầy đủ. Book.h và Thesis.h phải #include "Renewable.h".

Bước 2. class Book : public Document, public Renewable, nhớ chữ public thứ hai (mục 5). Thêm một thuộc tính đếm số lần đã gia hạn, khởi tạo bằng 0 trong danh sách khởi tạo. renew(days): đã đủ 2 lần thì trả false; còn lượt thì tăng bộ đếm, in dòng Gia han them <days> ngay. thụt bốn dấu cách như kết quả mong đợi, trả true. renewLeft() trả số lượt còn lại.

Bước 3. Làm TODO 3 trong Thesis: như Book nhưng tối đa 1 lần.

Bước 4. Làm TODO 3 trong main: với từng mã, dynamic_cast<Renewable*>(d), kiểm tra nullptr trước (mục 4), rồi in theo kết quả mong đợi. Biên dịch, so toàn bộ 19 dòng.

Nếu sách in khong gia han duoc: Book.h thiếu chữ public trước Renewable (mục 5). Một lần chạy thật:

text
--- 9.2 TAI LIEU NAO GIA HAN DUOC? ---
  Lap trinh huong doi tuong: khong gia han duoc.

Nếu chương trình dừng ngay sau dòng gia hạn của sách mà không báo lỗi: main chưa kiểm tra nullptr, nên chết khi tới tạp chí:

text
--- 9.2 TAI LIEU NAO GIA HAN DUOC? ---
  Lap trinh huong doi tuong: con 2 lan gia han.
    Gia han them 7 ngay.
    -> con lai 1 lan.

Nếu số lần gia hạn in ra một số lạ: quên khởi tạo bộ đếm bằng 0. Giá trị lúc đó là rác, có lần chạy tình cờ vẫn ra đúng, nên đừng lấy một lần chạy đúng làm bằng chứng.

Kết quả mong đợi ​

text
--- DANH MUC Thu vien Khoa ---
[SACH] S001 | Lap trinh huong doi tuong | Bjarne Stroustrup | 2019 | 350 trang | Con 2 ban
[TAP CHI] TC001 | Vat ly Ngay nay | So 57/9/2024 | Con 4 ban
[DIA] D001 | Thi nghiem quang hoc | 2022 | 45 phut | Con 1 ban
[LUAN VAN] LV001 | Mang nano TiO2 | GVHD TS. Nguyen Van B | Vat ly chat ran | 2025 | Con 1 ban
--- 9.1 MOI LOAI TU KHAI BAO MINH LA GI ---
  S001 -> loai: Sach, han muon 14 ngay
  TC001 -> loai: Tap chi, han muon 3 ngay
  D001 -> loai: Dia, han muon 2 ngay
  LV001 -> loai: Luan van, han muon 7 ngay
--- 9.2 TAI LIEU NAO GIA HAN DUOC? ---
  Lap trinh huong doi tuong: con 2 lan gia han.
    Gia han them 7 ngay.
    -> con lai 1 lan.
  Vat ly Ngay nay: khong gia han duoc.
  Thi nghiem quang hoc: khong gia han duoc.
  Mang nano TiO2: con 1 lan gia han.
    Gia han them 7 ngay.
    -> con lai 0 lan.

Thư mục bài làm — giữ trên máy, không nộp ​

text
<MSSV>_w09/
  (toàn bộ file tuần 8, đã sửa)
  Renewable.h
  Thesis.h/.cpp
  nhan_xet.txt     (thông báo lỗi bài 9.1 + trả lời bài 9.2)

Bài tại lớp không nộp. Tuần 9 chỉ nộp bài về nhà ở cuối trang. Giữ nguyên thư mục này: tuần 10 làm tiếp trên chính nó.

Thang điểm — tự chấm bài tại lớp ​

Bảng này không dùng để lấy điểm; bạn tự soi bài mình trước khi xem đáp án.

Nội dungĐiểm
Tính trừu tượng: thiết kế lớp trừu tượng đúng2
Bốn lớp con cài đặt đủ hàm thuần ảo2
Giao diện Renewable và dynamic_cast hoạt động đúng2
Chương trình chạy đúng2
nhan_xet.txt trả lời đúng trọng tâm1
Chia file, biên dịch một lệnh1

Bài về nhà — phần duy nhất phải nộp ​

Thêm int Library::demTheoLoai(const std::string& loai) const — đếm số tài liệu có typeName() trùng với chuỗi truyền vào. Gọi thử với "Sach" và "Luan van". Hạn nộp: trước buổi tuần 10. Nén các file đã sửa và main.cpp thành <MSSV>_w09_bvn.zip rồi nộp lên Moodle.

Đáp án tuần này

Nộp bài xong rồi mở trang đáp án tuần 9 và gõ mã mở đáp án giảng viên đăng trên Moodle. Mã đó chỉ hiện ra sau khi bạn đã nộp bài: đọc đáp án trước khi tự làm thì buổi lab thành vô ích.

© 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