Skip to content

Bài lab · Tuần 6

Tuần 6 — Tính kế thừa ​

Tuần này thêm vào hệ thống thư viện: Document sinh ra Book và Magazine; Reader sinh ra StudentReader rồi GradReader (kế thừa ba tầng).

Mang gì từ tuần trước ​

Chép <MSSV>_w05/ thành <MSSV>_w06/. Lớp Library giữ nguyên, không sửa dòng nào — nhưng nó vẫn phải biên dịch được. Đó là một phần của bài.

Mục tiêu ​

  • Phân biệt private và protected.
  • Gọi constructor lớp cha từ constructor lớp con bằng danh sách khởi tạo.
  • Dựng chuỗi kế thừa ba tầng và hiểu thứ tự constructor chạy.
  • Nhìn thấy tận mắt giới hạn của kế thừa khi chưa có virtual — đây là cửa vào tuần 7.

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 6 (.zip)

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

Tuần này Document sinh ra Book và Magazine, còn Reader sinh ra một chuỗi ba tầng. Ví dụ bên dưới vẫn ở chuyện âm nhạc: Track (bản ghi âm) sinh ra Song và Podcast, rồi chuỗi tài khoản nghe nhạc Listener → PremiumListener → FamilyListener. Mọi đoạn code đều đã biên dịch và chạy thật.

1. Kế thừa: lớp con "là một" lớp cha ​

Bài hát là một bản ghi âm, podcast cũng là một bản ghi âm. Phần chung (tựa, thời lượng, số phút) đặt ở lớp cha Track; mỗi lớp con chỉ thêm phần riêng của mình. Trên sơ đồ, mũi tên có đầu tam giác rỗng chỉ từ lớp con lên lớp cha:

Sơ đồ UML: lớp Track có hai thuộc tính protected title, seconds và ba phương thức; hai lớp con Song (artist) và Podcast (host, episode), mỗi lớp có constructor và show riêngSơ đồ UML: lớp Track có hai thuộc tính protected title, seconds và ba phương thức; hai lớp con Song (artist) và Podcast (host, episode), mỗi lớp có constructor và show riêng
Thấy trên UMLViết trong C++
Mũi tên tam giác rỗng từ Song lên Trackclass Song : public Track { ... };
Dấu # ở đầu dòngđặt dòng đó dưới protected: (mục 2)
Hộp Song không có title, seconds, minutes()lớp con tự có sẵn từ lớp cha, không khai báo lại
Hộp Song có lại dòng + show() : voidlớp con viết lại hàm đó theo cách riêng

Cho gọn, cả ba lớp nằm chung một file (mục 5 chia file):

cpp
#include <iostream>
#include <string>
using namespace std;

class Track {
protected:
    string title;
    int seconds;

public:
    Track(const string& title, int seconds) : title(title), seconds(seconds) {}

    int minutes() const { return seconds / 60; }

    void show() const {
        cout << "[BAN GHI] " << title << " | " << seconds << " giay" << endl;
    }
};

class Song : public Track {
private:
    string artist;

public:
    Song(const string& title, int seconds, const string& artist)
        : Track(title, seconds), artist(artist) {}

    void show() const {
        cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
    }
};

class Podcast : public Track {
private:
    string host;
    int episode;

public:
    Podcast(const string& title, int seconds, const string& host, int episode)
        : Track(title, seconds), host(host), episode(episode) {}

    void show() const {
        cout << "[PODCAST] " << title << " | Tap " << episode << " | " << host << " | " << seconds << " giay" << endl;
    }
};

int main() {
    Track t("Ban thu am", 95);
    Song s("Hello Viet Nam", 263, "Pham Quynh Anh");
    Podcast p("Chuyen lap trinh", 1800, "Minh Tu", 12);
    t.show();
    s.show();
    p.show();
    cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl;   // minutes() ke thua tu Track
    return 0;
}
text
[BAN GHI] Ban thu am | 95 giay
[BAI HAT] Hello Viet Nam | Pham Quynh Anh | 263 giay
[PODCAST] Chuyen lap trinh | Tap 12 | Minh Tu | 1800 giay
Bai hat dai khoang 4 phut
  • s.minutes() gọi được dù Song không viết hàm minutes: hàm đó kế thừa từ Track.
  • Ba đối tượng gọi ba bản show() khác nhau: gọi trên biến kiểu nào thì chạy bản của lớp đó.
  • Constructor của Song và Podcast bắt đầu bằng Track(title, seconds) (mục 3).

2. protected: lớp con dùng được, bên ngoài vẫn không ​

show() của Song đọc thẳng title và seconds. Nếu hai thuộc tính này là private như tuần 5, g++ chặn luôn cả lớp con (trích):

text
main.cpp: In member function 'void Song::show() const':
main.cpp:29:33: error: 'std::string Track::title' is private within this context
   29 |         cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
      |                                 ^~~~~
main.cpp:7:12: note: declared private here
    7 |     string title;
      |            ^~~~~

Đổi thành protected thì lớp con dùng được, còn main vẫn bị chặn như cũ:

text
main.cpp: In function 'int main()':
main.cpp:51:15: error: 'std::string Track::title' is protected within this context
   51 |     cout << s.title << endl;
      |               ^~~~~
main.cpp:7:12: note: declared protected here
    7 |     string title;
      |            ^~~~~
Thuộc tính khai báo làTrong chính lớp đóTrong lớp conBên ngoài, ví dụ main
privateđượckhôngkhông
protectedđượcđượckhông
publicđượcđượcđược

Chỉ dùng protected cho thuộc tính mà lớp con thật sự cần đọc. Thuộc tính không lớp con nào cần thì cứ để private, như artist của Song.

3. Constructor lớp con gọi constructor lớp cha ​

Phần Track bên trong một Song phải do constructor của Track dựng. Vì vậy constructor của lớp con gọi constructor lớp cha ở đầu danh sách khởi tạo, rồi mới tới thuộc tính của riêng nó:

cpp
    Song(const string& title, int seconds, const string& artist)
        : Track(title, seconds), artist(artist) {}

Quên gọi constructor lớp cha. C++ tự gọi constructor mặc định Track(). Track không có constructor đó nên g++ báo (trích):

text
main.cpp: In constructor 'Song::Song(const std::string&, int, const std::string&)':
main.cpp:26:24: error: no matching function for call to 'Track::Track()'
   26 |         : artist(artist) {}
      |                        ^
main.cpp:26:24: note: there are 3 candidates
main.cpp:11:5: note: candidate 1: 'Track::Track(const std::string&, int)'
   11 |     Track(const string& title, int seconds) : title(title), seconds(seconds) {}
      |     ^~~~~
main.cpp:11:5: note: candidate expects 2 arguments, 0 provided

Nếu lớp cha có constructor mặc định thì không lỗi gì cả: phần lớp cha lặng lẽ nhận giá trị mặc định. Document của bạn đang có constructor mặc định, nên ở Bài 6.1 đây là một lỗi âm thầm.

Tự khởi tạo thuộc tính của lớp cha. Danh sách khởi tạo của Song chỉ nhận thuộc tính khai báo trong chính Song và lớp cha trực tiếp của nó (trích):

text
main.cpp: In constructor 'Song::Song(const std::string&, int, const std::string&)':
main.cpp:26:11: error: class 'Song' does not have any field named 'title'
   26 |         : title(title), seconds(seconds), artist(artist) {}
      |           ^~~~~
main.cpp:26:25: error: class 'Song' does not have any field named 'seconds'
   26 |         : title(title), seconds(seconds), artist(artist) {}
      |                         ^~~~~~~
main.cpp:26:56: error: no matching function for call to 'Track::Track()'

Một Song vẫn chứa title, nhưng title thuộc phần Track, nên phải để constructor Track khởi tạo.

4. Nhiều tầng kế thừa và thứ tự constructor ​

Kế thừa có thể nối nhiều tầng. Tài khoản miễn phí nghe trên 1 thiết bị. Tài khoản Premium là một tài khoản có trả phí, được 3 thiết bị. Gói gia đình là một tài khoản Premium dùng chung, được 6 thiết bị:

Sơ đồ UML chuỗi kế thừa ba tầng Listener, PremiumListener, FamilyListener; mỗi lớp có constructor, maxDevices trả về 1, 3, 6 và show riêngSơ đồ UML chuỗi kế thừa ba tầng Listener, PremiumListener, FamilyListener; mỗi lớp có constructor, maxDevices trả về 1, 3, 6 và show riêng

Mỗi constructor in một dòng để thấy nó chạy lúc nào:

cpp
#include <iostream>
#include <string>
using namespace std;

class Listener {
protected:
    string name;

public:
    Listener(const string& name) : name(name) {
        cout << "  -> constructor Listener" << endl;
    }

    int maxDevices() const { return 1; }

    void show() const {
        cout << "[MIEN PHI] " << name << " | " << maxDevices() << " thiet bi" << endl;
    }
};

class PremiumListener : public Listener {
protected:
    int months;                          // so thang da tra truoc

public:
    PremiumListener(const string& name, int months) : Listener(name), months(months) {
        cout << "  -> constructor PremiumListener" << endl;
    }

    int maxDevices() const { return 3; }

    void show() const {
        cout << "[PREMIUM] " << name << " | Con " << months << " thang | " << maxDevices() << " thiet bi" << endl;
    }
};

class FamilyListener : public PremiumListener {
private:
    int members;

public:
    FamilyListener(const string& name, int months, int members)
        : PremiumListener(name, months), members(members) {
        cout << "  -> constructor FamilyListener" << endl;
    }

    int maxDevices() const { return 6; }

    void show() const {
        cout << "[GIA DINH] " << name << " | Con " << months << " thang | " << members << " nguoi | "
             << maxDevices() << " thiet bi" << endl;
    }
};

int main() {
    cout << "Tao Listener:" << endl;
    Listener a("An");
    cout << "Tao FamilyListener:" << endl;
    FamilyListener f("Nha Binh", 12, 4);
    a.show();
    f.show();
    return 0;
}
text
Tao Listener:
  -> constructor Listener
Tao FamilyListener:
  -> constructor Listener
  -> constructor PremiumListener
  -> constructor FamilyListener
[MIEN PHI] An | 1 thiet bi
[GIA DINH] Nha Binh | Con 12 thang | 4 nguoi | 6 thiet bi
  • Tạo một FamilyListener làm chạy cả ba constructor. Đọc output để thấy thứ tự.
  • Mỗi tầng chỉ gọi lớp cha trực tiếp của mình: FamilyListener gọi PremiumListener, rồi PremiumListener tự gọi Listener. Gọi vượt tầng thì g++ báo (trích):
text
main.cpp: In constructor 'FamilyListener::FamilyListener(const std::string&, int, int)':
main.cpp:43:11: error: type 'Listener' is not a direct base of 'FamilyListener'
   43 |         : Listener(name), members(members) {
      |           ^~~~~~~~
main.cpp:43:42: error: no matching function for call to 'PremiumListener::PremiumListener()'
  • show() của FamilyListener dùng được name và months dù chúng khai báo ở hai tầng trên, vì cả hai đều protected.

5. Chia file cho lớp con ​

Mỗi lớp vẫn một cặp .h / .cpp như tuần 4. Điểm mới duy nhất: file .h của lớp con phải #include file .h của lớp cha.

cpp
#ifndef TRACK_H
#define TRACK_H

#include <string>

class Track {
protected:
    std::string title;
    int seconds;

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

    int minutes() const;
    void show() const;
};

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

Track::Track(const string& title, int seconds) : title(title), seconds(seconds) {}

int Track::minutes() const { return seconds / 60; }

void Track::show() const {
    cout << "[BAN GHI] " << title << " | " << seconds << " giay" << endl;
}
cpp
#ifndef SONG_H
#define SONG_H

#include "Track.h"          // phai thay tron lop cha truoc dong "class Song : public Track"

class Song : public Track {
private:
    std::string artist;

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

    void show() const;
};

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

Song::Song(const string& title, int seconds, const string& artist)
    : Track(title, seconds),        // goi constructor lop cha TRUOC
      artist(artist) {}

void Song::show() const {
    cout << "[BAI HAT] " << title << " | " << artist << " | " << seconds << " giay" << endl;
}
cpp
#include "Song.h"
#include <iostream>
using namespace std;

int main() {
    Song s("Hello Viet Nam", 263, "Pham Quynh Anh");
    s.show();
    cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl;
    return 0;
}
sh
g++ -std=c++17 -Wall Track.cpp Song.cpp main.cpp -o main.exe
text
[BAI HAT] Hello Viet Nam | Pham Quynh Anh | 263 giay
Bai hat dai khoang 4 phut
  • main.cpp chỉ include Song.h; Track đi theo nhờ dòng #include "Track.h" trong Song.h.
  • Lệnh g++ vẫn phải có Track.cpp, vì thân các hàm của Track nằm ở đó.
  • Thư mục nhiều file thì gõ g++ -std=c++17 -Wall *.cpp -o main.exe để biên dịch mọi file .cpp trong thư mục, trong PowerShell hay cmd đều được. Chỉ dùng cách này khi thư mục không có file .cpp nào thừa.

Quên #include "Track.h" trong Song.h. Lỗi đầu tiên là dòng đáng đọc. Phía sau là cả loạt lỗi kéo theo, có dòng còn gợi ý sai hướng là thêm #include <string> (trích):

text
In file included from Song.cpp:1:
Song.h:5:27: error: expected class-name before '{' token
    5 | class Song : public Track {
      |                           ^
Song.h:7:10: error: 'string' in namespace 'std' does not name a type
    7 |     std::string artist;
      |          ^~~~~~
Song.h:1:1: note: 'std::string' is defined in header '<string>'; this is probably fixable by adding '#include <string>'

Quên chữ public. Viết class Song : Track thì mọi thứ kế thừa từ Track bị giấu với bên ngoài. Trên ví dụ một file ở mục 1:

text
main.cpp: In function 'int main()':
main.cpp:54:47: error: 'int Track::minutes() const' is inaccessible within this context
   54 |     cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl;   // minutes() ke thua tu Track
      |                                      ~~~~~~~~~^~
main.cpp:13:9: note: declared here
   13 |     int minutes() const { return seconds / 60; }
      |         ^~~~~~~
main.cpp:54:47: error: 'Track' is not an accessible base of 'Song'
   54 |     cout << "Bai hat dai khoang " << s.minutes() << " phut" << endl;   // minutes() ke thua tu Track
      |                                      ~~~~~~~~~^~

Bài 6.1 — Kế thừa đơn: Book và Magazine (40 phút) ​

Mục tiêu 40 phút40:00Chưa bắt đầu

Bước 1. Trong Document.h, đổi private: thành protected:. Trả lời được: vì sao phải đổi? private chặn ai?

Bước 2. Viết hai lớp con:

Sơ đồ UML: Document có bốn thuộc tính protected; hai lớp con Book (author, pages) và Magazine (issueNo, month), mỗi lớp có constructor và display riêngSơ đồ UML: Document có bốn thuộc tính protected; hai lớp con Book (author, pages) và Magazine (issueNo, month), mỗi lớp có constructor và display riêng
  • Book(id, title, year, copies, author, pages)
  • Magazine(id, title, year, copies, issueNo, month)

Constructor lớp con bắt buộc gọi constructor lớp cha ở danh sách khởi tạo:

cpp
Book::Book(const string& id, const string& title, int year, int copies,
           const string& author, int pages)
    : Document(id, title, year, copies),   // goi lop cha TRUOC
      author(author), pages(pages) {}

Mỗi lớp con viết lại display() theo định dạng riêng của mình.

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>_w05 thành <MSSV>_w06, tải khung bài làm tuần 6 và chép mọi file trong đó vào thư mục mới. main.cpp của khung thay cho main.cpp cũ. Biên dịch thử bằng lệnh trong HUONG_DAN.txt: chưa in gì nhưng phải qua.

Bước 2. Trong Document.h, đổi private: thành protected:. Muốn tự thấy vì sao phải đổi, làm xong bước 5 rồi tạm đổi lại private: và biên dịch (trích):

text
Book.cpp: In member function 'void Book::display() const':
Book.cpp:13:26: error: 'std::string Document::id' is private within this context
   13 |     cout << "[SACH] " << id << " | " << title << " | " << author
      |                          ^~
In file included from Book.h:4,
                 from Book.cpp:1:
Document.h:10:17: note: declared private here

Book không đọc được id, vì private chặn cả lớp con (mục 2). Xem xong nhớ đổi lại protected:.

Bước 3. Viết Book.h theo sơ đồ: #include "Document.h", class Book : public Document, hai thuộc tính private, constructor sáu tham số và display() const. Không khai báo lại id, title, year, copies (mục 1).

Bước 4. Viết constructor trong Book.cpp đúng như mẫu trong đề: Document(id, title, year, copies) đứng đầu danh sách khởi tạo (mục 3).

Bước 5. Viết Book::display() theo dòng [SACH] của kết quả mong đợi. Để ý thứ tự: tác giả đứng trước năm.

Bước 6. Làm Magazine tương tự. Dòng [TAP CHI] in số, tháng, năm nối bằng dấu /, ví dụ So 57/9/2024.

Bước 7. Trong main.cpp, tạo Book b và Magazine m với dữ liệu lấy từ kết quả mong đợi, rồi gọi display(). Biên dịch, so hai dòng đầu.

Nếu dòng [SACH] in ra ??? | Chua dat ten và năm là 0 mà g++ không cảnh báo gì: constructor của Book chưa gọi constructor Document, nên C++ dùng constructor mặc định của Document (mục 3). Một lần chạy thật với lỗi này:

text
--- 6.1 KE THUA DON ---
[SACH] ??? | Chua dat ten | Bjarne Stroustrup | 0 | 350 trang | Con 0 ban
[TAP CHI] TC001 | Vat ly Ngay nay | So 57/9/2024 | Con 4 ban

Nếu gặp class 'Book' does not have any field named 'id': bạn khởi tạo thuộc tính của Document ngay trong danh sách của Book. Thay bằng một lời gọi Document(id, title, year, copies).

Nếu lỗi đầu tiên là expected class-name before '{' token ở dòng class Book : public Document: Book.h thiếu #include "Document.h". Đừng làm theo gợi ý thêm #include <string> ở các dòng lỗi phía sau (mục 5).


Bài 6.2 — Kế thừa ba tầng: bạn đọc (40 phút) ​

Mục tiêu 40 phút40:00Chưa bắt đầu
Sơ đồ UML chuỗi kế thừa ba tầng Reader, StudentReader, GradReader; mỗi lớp có constructor, maxBorrow trả về 3, 5, 10 và display riêngSơ đồ UML chuỗi kế thừa ba tầng Reader, StudentReader, GradReader; mỗi lớp có constructor, maxBorrow trả về 3, 5, 10 và display riêng

Bước 1 — sửa Reader của tuần 5. Ba thuộc tính id, fullName, borrowing đang để public: đổi thành protected y như đã làm với Document ở bài 6.1. Thêm hai hàm:

  • std::string getFullName() const — bên ngoài vẫn đọc được tên dù thuộc tính đã đóng (tuần 8 dùng lại hàm này).
  • int maxBorrow() const — bạn đọc thường: 3 tài liệu.

Library không đụng tới Reader nên đổi xong vẫn biên dịch bình thường.

Bước 2 — viết hai lớp con:

  • Reader(id, fullName) — hạn mức 3 tài liệu.
  • StudentReader(id, fullName, studentId, faculty) — hạn mức 5.
  • GradReader(id, fullName, studentId, faculty, researchField) — hạn mức 10.

Mỗi lớp có maxBorrow() trả hạn mức riêng và display() riêng — định dạng in xem ở mục Kết quả mong đợi.

Câu hỏi phải trả lời: khi tạo một GradReader, constructor nào chạy trước — của GradReader, StudentReader hay Reader? Chèn cout vào từng constructor để tự kiểm chứng, rồi xóa đi trước khi nộp.

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

Bước 1. Sửa Reader.h theo sơ đồ: thuộc tính chuyển từ public sang protected, thêm maxBorrow() const. Constructor có tham số giữ nguyên. Constructor mặc định của tuần 4 không có trên sơ đồ; bỏ hay giữ đều biên dịch được.

Bước 2. Trong Reader.cpp: maxBorrow() trả về 3; display() đổi sang định dạng [BAN DOC], in hạn mức bằng cách gọi maxBorrow().

Bước 3. Viết StudentReader kế thừa Reader. Constructor gọi Reader(id, fullName) trước. Hai thuộc tính studentId, faculty để protected, vì GradReader ở tầng dưới cần in chúng. maxBorrow() trả 5, display() theo dòng [SINH VIEN].

Bước 4. Viết GradReader kế thừa StudentReader. Constructor năm tham số gọi StudentReader(id, fullName, studentId, faculty), không gọi thẳng Reader (mục 4). maxBorrow() trả 10, display() theo dòng [CAO HOC].

Bước 5. Trong main.cpp, tạo ba bạn đọc theo kết quả mong đợi và gọi display(). Biên dịch, so ba dòng của phần 6.2.

Bước 6. Trả lời câu hỏi của đề bằng thực nghiệm: tạm thêm một dòng cout vào thân constructor của cả ba lớp, ví dụ trong Reader.cpp:

cpp
Reader::Reader(const string& id, const string& fullName)
    : id(id), fullName(fullName), borrowing(0) {
    cout << "  (constructor Reader)" << endl;
}

Chạy lại, xem việc tạo GradReader in ra những dòng nào, theo thứ tự nào. Ghi lại, rồi xoá cả ba dòng cout sau khi thử xong.

Nếu gặp type 'Reader' is not a direct base of 'GradReader': constructor của GradReader đang gọi vượt tầng (bước 4). Trích:

text
GradReader.cpp: In constructor 'GradReader::GradReader(const std::string&, const std::string&, const std::string&, const std::string&, const std::string&)':
GradReader.cpp:8:7: error: type 'Reader' is not a direct base of 'GradReader'
    8 |     : Reader(id, fullName), researchField(researchField) {}
      |       ^~~~~~

Nếu gặp 'std::string StudentReader::studentId' is private within this context trong GradReader.cpp: thuộc tính của StudentReader còn private (bước 3).

Nếu dòng của sinh viên in ra [BAN DOC] ... Han muc 3 tai lieu: StudentReader chưa có display() riêng, nên s.display() chạy bản kế thừa từ Reader, và bản đó gọi maxBorrow() của Reader:

text
[BAN DOC] BD001 | Nguyen Van An | Han muc 3 tai lieu
[BAN DOC] BD002 | Tran Thi Binh | Han muc 3 tai lieu

Bài 6.3 — Quan sát: gọi qua con trỏ lớp cha (20 phút) ​

Mục tiêu 20 phút20:00Chưa bắt đầu

Thêm vào cuối main.cpp dòng tiêu đề --- 6.3 ... --- rồi đoạn sau. Nếu bài 6.1 đã tạo sẵn Book b với đúng dữ liệu này thì bỏ dòng đầu, dùng luôn biến đó: khai báo b lần nữa sẽ bị g++ báo redeclaration of 'Book b'.

cpp
Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
Document* p = &b;      // con tro Document tro toi mot Book
p->display();          // goi ham NAO?

Chạy và ghi lại kết quả. Bạn sẽ thấy nó in ra dòng [TL] ... — tức là bản của lớp cha, dù đối tượng thật sự là một Book.

Viết vào file nhan_xet.txt (3–5 dòng):

  • Bạn chờ đợi kết quả nào, và thực tế in ra gì?
  • Vì sao trình biên dịch lại chọn hàm của lớp cha?
  • Điều này gây rắc rối gì cho Library::listAll() — vốn duyệt một mảng Document*?

Tuần 7 sẽ sửa chỗ này bằng đúng một từ khóa.

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

Bước 1. Ở cuối main, in dòng tiêu đề --- 6.3 QUAN SAT: goi qua con tro lop cha ---, cho con trỏ Document* p trỏ tới Book b đã tạo ở bài 6.1, rồi gọi p->display(). Chép nguyên đoạn trong đề mà main đã có sẵn b thì g++ báo:

text
main.cpp: In function 'int main()':
main.cpp:25:10: error: redeclaration of 'Book b'
   25 |     Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
      |          ^
main.cpp:11:10: note: 'Book b' previously declared here
   11 |     Book b("S001", "Lap trinh huong doi tuong", 2019, 2, "Bjarne Stroustrup", 350);
      |          ^

Khi đó bỏ dòng tạo b thứ hai.

Bước 2. Chạy, so với kết quả mong đợi. Dòng cuối là [TL] ..., đúng như đề đã báo trước.

Bước 3. Trước khi viết nhan_xet.txt, làm thêm một thí nghiệm nhỏ. Ngay sau p->display();, thêm:

cpp
    Book* q = &b;              // cung doi tuong b, nhung con tro kieu Book*
    q->display();

p và q trỏ tới cùng một đối tượng. Chạy lại và so hai dòng cuối:

text
[TL] S001 | Lap trinh huong doi tuong | 2019 | Con 2 ban
[SACH] S001 | Lap trinh huong doi tuong | Bjarne Stroustrup | 2019 | 350 trang | Con 2 ban

Bước 4. Viết nhan_xet.txt. Mấy câu hỏi dưới đây giúp bạn tự tìm câu trả lời, không phải đáp án:

  • Hai lời gọi ở bước 3 khác nhau ở chỗ nào: ở đối tượng, hay ở kiểu của con trỏ?
  • Ở dòng p->display();, trình biên dịch biết chắc điều gì về p, và điều gì nó không biết được?
  • Library::listAll() gọi items[i]->display(). items[i] có kiểu gì? Nếu thư viện chứa toàn Book và Magazine thì mỗi dòng in ra sẽ bắt đầu bằng gì?

Xoá hai dòng thí nghiệm sau khi thử xong: main.cpp của bài phải in đúng chín dòng của kết quả mong đợi.

Kết quả mong đợi ​

text
--- 6.1 KE THUA DON ---
[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
--- 6.2 KE THUA DA CAP (3 tang constructor) ---
[BAN DOC] BD001 | Nguyen Van An | Han muc 3 tai lieu
[SINH VIEN] BD002 | Tran Thi Binh | MSSV 23130007 | Khoa Vat ly | Han muc 5
[CAO HOC] BD003 | Le Minh Cuong | MSSV 22C12345 | Khoa Vat ly | Huong NC: Vat ly chat ran | Han muc 10
--- 6.3 QUAN SAT: goi qua con tro lop cha ---
[TL] S001 | Lap trinh huong doi tuong | 2019 | Con 2 ban

Dòng cuối đúng là như vậy — nó không phải lỗi của bạn. Đó là nội dung bài 6.3.

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

text
<MSSV>_w06/
  Document.h/.cpp   Book.h/.cpp   Magazine.h/.cpp
  Reader.h/.cpp     StudentReader.h/.cpp   GradReader.h/.cpp
  Library.h/.cpp    (giữ nguyên tuần 5)
  Newspaper.h/.cpp  (bài về nhà, xem cuối trang)
  main.cpp
  nhan_xet.txt

Bài tại lớp không nộp. Tuần 6 chỉ nộp bài về nhà ở cuối trang. Giữ nguyên thư mục này: tuần 7 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
Chương trình chạy đúng2
Book, Magazine kế thừa đúng, gọi constructor cha ở danh sách khởi tạo2
Chuỗi ba tầng Reader → StudentReader → GradReader đúng2
Dùng protected đúng chỗ, giải thích được1
nhan_xet.txt của bài 6.3 trả lời đúng trọng tâm2
Code tuần 5 vẫn biên dịch được1

Bài về nhà — chấm điểm ​

Thêm lớp Newspaper (báo ngày) kế thừa Document. Chỉ một lớp, nhưng nó kiểm tra đúng ba thứ của tuần này: gọi constructor lớp cha, dùng lại hàm của lớp cha, và viết display() riêng cho lớp con.

Đo xem bạn mất bao nhiêu phút và ghi con số đó vào dòng chú thích đầu Newspaper.h. Tuần 9 sẽ thêm một lớp nữa và ta so lại con số đó.

Yêu cầu ​

Document giữ nguyên bản tuần 6 của bạn — tức là bốn thuộc tính đã ở protected, không sửa thêm gì.

Lớp Newspaper thêm đúng một thuộc tính publishDay kiểu std::string, dạng dd/mm/yyyy (ví dụ "16/10/2026"). Máy chấm so output từng ký tự nên ba chữ ký dưới đây là hợp đồng, giữ nguyên y nguyên:

cpp
class Newspaper : public Document {
private:
    std::string publishDay;

public:
    Newspaper(const std::string& id, const std::string& title, int year, int copies,
              const std::string& publishDay);
    std::string getPublishDay() const;
    void display() const;
};
  • Constructor bắt buộc gọi constructor Document ở danh sách khởi tạo, đúng như Book ở bài 6.1: : Document(id, title, year, copies), publishDay(publishDay) {}. Quên gọi lớp cha thì bốn thuộc tính kia nhận giá trị mặc định (???, Chua dat ten, 0, 0) và rớt cả bốn bộ kiểm thử.

  • display() const in đúng một dòng, theo định dạng:

    text
    [BAO] <id> | <title> | Ra ngay <publishDay> | Con <copies> ban

Chữ const ở cuối không được thiếu. Máy chấm in tờ báo qua tham chiếu hằng const Newspaper&, nên display(), getPublishDay() và các getter của Document đều phải có const ở cuối. Thiếu một chữ là không biên dịch được, 0 điểm — đúng cái bẫy đã gặp ở bài về nhà tuần 4, lần này tránh được rồi.

Máy chấm gọi đúng những hàm này ​

cpp
Newspaper* t = new Newspaper("BND001", "Tuoi Tre", 2026, 5, "16/10/2026");
inBao(*t);              // -> display() const, qua tham chieu const
t->getId();  t->getTitle();  t->getYear();  t->getCopies();   // getter cua lop cha
t->getPublishDay();
t->borrow();  t->giveBack();  t->available();                 // ham cua lop cha, dung lai duoc
t->setYear(2030);       // setter cua lop cha van phai chan nam sai

Không nộp main.cpp: máy đã có main riêng để chạy các kịch bản này.

Kết quả mong đợi của bài về nhà ​

Một trong bốn bộ kiểm thử, để bạn tự so trước khi nộp:

text
--- TAO BAO ---
[BAO] BND001 | Tuoi Tre | Ra ngay 16/10/2026 | Con 5 ban
[BAO] BND002 | Thanh Nien | Ra ngay 15/10/2026 | Con 3 ban
[BAO] BND003 | Khoa hoc Pho thong | Ra ngay 10/10/2026 | Con 2 ban
--- NGAY RA BAO ---
Ngay ra bao: 16/10/2026
Ngay ra bao: 10/10/2026

Chấm điểm ​

Mười điểm chia đều cho bốn bộ kiểm thử, mỗi bộ 2,5 điểm.

Bộ kiểm thửĐiểm
Tạo ba tờ báo, in ra, đọc ngày ra báo2,5
Getter và mượn/trả của lớp cha chạy trên lớp con2,5
setYear vẫn chặn năm sai khi gọi trên Newspaper2,5
Nhiều tờ báo: tờ hết bản, tờ còn bản, mã không có2,5

Nộp sau hạn thì trừ 10% điểm bài đó — được 10 điểm 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.

Nộp bài ​

  • Nộp đúng bốn tệp: Document.h, Document.cpp, Newspaper.h, Newspaper.cpp.
  • Không nộp main.cpp, cũng không nộp Book.*, Magazine.*, Reader.*, Library.*.
  • Trong hoạt động Phòng lập trình ảo trên Moodle, 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.
  • 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 7 của lớp bạn: lớp học thứ hai — 19:00 chủ nhật 25/10; lớp học thứ tư — 19:00 thứ ba 27/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 6 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