Giao diện
Bài lab · Tuần 4
Tuần 4 — Từ UML sang lớp đầu tiên
Tuần này thêm vào hệ thống thư viện: hai lớp đầu tiên —
Document(tài liệu) vàReader(bạn đọc). Đây là nền móng, mọi tuần sau đều xây tiếp lên hai lớp này.
Mang gì từ tuần trước
Chưa có gì. Tuần 4 bắt đầu một thư mục mới <MSSV>_w04/. Nhưng từ tuần 5 trở đi bạn sẽ chép thư mục tuần trước sang và sửa tiếp, nên hãy giữ code sạch ngay từ bây giờ.
Mục tiêu
- Đọc một sơ đồ UML và dịch thành lớp C++.
- Tách mỗi lớp thành hai file: khai báo
.hvà cài đặt.cpp. - Biên dịch nhiều file bằng một lệnh
g++. - Viết hàm khởi tạo (constructor) có tham số và constructor mặc định.
Quy ước viết file (áp dụng cả học kỳ)
- File
.hchứa khai báo: tên lớp, thuộc tính, nguyên mẫu hàm. Không viết thân hàm ở đây. - File
.cppchứa cài đặt: thân của từng hàm, viết dạngTenLop::tenHam(...). - Trong
.hdùngstd::string, không viếtusing namespace std;. Trong.cppthì được phépusing namespace std;. - Mỗi
.hphải có "chốt chặn" để không bị include hai lần:cpp#ifndef DOCUMENT_H #define DOCUMENT_H // ... khai bao ... #endif
Khung bài làm
Tải về, giải nén rồi mở thư mục bằng công cụ bạn dùng. Trong đó có sẵn các file cần nộp với những chỗ ghi TODO để bạn tự viết, cùng file HUONG_DAN.txt ghi cách biên dịch.
Lý thuyết cần nắm
Tuần này bạn viết lớp đầu tiên. Mọi ví dụ dưới đây dùng một lớp khác hẳn bài lab, lớp Song (bài hát), và đều đã biên dịch, chạy thật. Đọc để nắm cách viết, rồi tự dịch sơ đồ của Document và vẽ sơ đồ của Reader.
1. Lớp và đối tượng
Lớp là cái khuôn: nó mô tả một loại đối tượng có những dữ liệu gì (thuộc tính) và làm được gì (phương thức). Đối tượng là một thứ cụ thể làm ra từ khuôn đó. Chưa chia file, cả chương trình nằm trong một file:
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 a; // tao doi tuong a tu khuon Song
a.title = "Hello Viet Nam";
a.seconds = 263;
a.show(); // goi phuong thuc cua a
return 0;
}text
Hello Viet Nam (4 phut 23 giay)public:cho phép dùng thuộc tính và phương thức từ bên ngoài lớp, ví dụ trongmain. Tuần 5 sẽ học cách đóng lại.- Dấu
;sau dấu}đóng lớp là bắt buộc. - Truy cập thuộc tính và gọi phương thức bằng dấu chấm:
a.title,a.show().
2. Đọc sơ đồ UML của một lớp
Sơ đồ lớp là một hộp ba tầng: tên lớp, thuộc tính, phương thức. Đây là sơ đồ của lớp Song:


Dịch từng dòng sang C++:
| Thấy trên UML | Viết trong file .h |
|---|---|
Hộp tên Song | class Song { ... }; |
Dấu + ở đầu dòng | đặt dòng đó dưới public: |
+ title : string | std::string title; (UML ghi tên trước kiểu sau, C++ thì ngược lại) |
+ seconds : int | int seconds; |
+ Song() | Song(); (constructor trùng tên lớp, không có kiểu trả về) |
+ Song(title, seconds) | Song(const std::string& title, int seconds); |
+ show() : void | void show() const; (hàm chỉ in nên thêm const, xem mục 4) |
Sơ đồ thường không ghi kiểu của tham số constructor: lấy kiểu của thuộc tính cùng tên.
3. Constructor: hàm chạy khi đối tượng ra đời
Ở mục 1 phải gán từng thuộc tính sau khi tạo đối tượng. Constructor làm việc đó ngay lúc tạo:
cpp
#include <iostream>
#include <string>
using namespace std;
class Song {
public:
string title;
int seconds;
Song() : title("Bai hat moi"), seconds(0) {}
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 a("Hello Viet Nam", 263); // constructor co tham so
Song b; // constructor mac dinh: KHONG co ngoac
a.show();
b.show();
return 0;
}text
Hello Viet Nam (4 phut 23 giay)
Bai hat moi (0 phut 0 giay)- Constructor mặc định không có tham số, chạy khi viết
Song b;. - Constructor có tham số nhận giá trị ngay lúc tạo:
Song a("Hello Viet Nam", 263);. - Phần sau dấu
:là danh sách khởi tạo. Trongtitle(title), chữ bên ngoài ngoặc là thuộc tính, chữ bên trong ngoặc là tham số. Đề bài bắt buộc dùng cách này.
Hai lỗi hay gặp với constructor:
Viết Song b(); thay vì Song b;. Có cặp ngoặc rỗng, C++ hiểu dòng đó là khai báo một hàm tên b chứ không phải tạo đối tượng. g++ cảnh báo ở dòng khai báo, rồi báo lỗi ở chỗ gọi b.show() (dòng 20 trong code của bạn còn kèm chú thích, nên g++ in ra cả chú thích đó):
text
main.cpp: In function 'int main()':
main.cpp:20:11: warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
20 | Song b();
| ^~
main.cpp:20:11: note: remove parentheses to default-initialize a variable
20 | Song b();
| ^~
| --
main.cpp:20:11: note: or replace parentheses with braces to value-initialize a variable
main.cpp:22:7: error: request for member 'show' in 'b', which is of non-class type 'Song()'
22 | b.show();
| ^~~~Gán trong thân hàm thay vì dùng danh sách khởi tạo:
cpp
Song(const string& title, int seconds) : seconds(seconds) {
title = title; // SAI: gan tham so title cho chinh no
}Trong thân constructor, chữ title là tham số, không phải thuộc tính, nên câu đó gán tham số cho chính nó. Tham số lại là const, nên g++ báo một lỗi dài hàng chục dòng. Chỉ cần đọc mấy dòng đầu (trích):
text
main.cpp: In constructor 'Song::Song(const std::string&, int)':
main.cpp:12:17: error: no match for 'operator=' (operand types are 'const std::string' {aka 'const std::__cxx11::basic_string<char>'} and 'const std::string' {aka 'const std::__cxx11::basic_string<char>'})
12 | title = title; // SAI: gan tham so title cho chinh no
| ^~~~~Sửa bằng danh sách khởi tạo như ví dụ phía trên; thân hàm để trống.
4. const ở cuối phương thức
void show() const hứa rằng show chỉ đọc, không sửa thuộc tính nào của đối tượng. Hàm in thông tin thì nên có const. Chữ const phải có ở cả hai nơi: nguyên mẫu trong .h và dòng định nghĩa trong .cpp. Thiếu ở .cpp, g++ báo không có khai báo nào khớp, rồi chỉ luôn bản đúng:
text
Song.cpp:10:6: error: no declaration matches 'void Song::show()'
10 | void Song::show() {
| ^~~~
In file included from Song.cpp:1:
Song.h:14:10: note: candidate is: 'void Song::show() const'
14 | void show() const;
| ^~~~
Song.h:6:7: note: 'class Song' defined here
6 | class Song {
| ^~~~5. Chia một lớp thành .h và .cpp
Giống bài 3.0 của tuần 3, chỉ khác là file .h giờ chứa một lớp: .h khai báo, .cpp cài đặt, main.cpp dùng.
cpp
#ifndef SONG_H
#define SONG_H
#include <string>
class Song {
public:
std::string title;
int seconds;
Song();
Song(const std::string& title, int seconds);
void show() const;
};
#endifcpp
#include "Song.h"
#include <iostream>
using namespace std;
Song::Song() : title("Bai hat moi"), seconds(0) {}
Song::Song(const string& title, int seconds)
: title(title), seconds(seconds) {}
void Song::show() const {
cout << title << " (" << seconds / 60 << " phut " << seconds % 60 << " giay)" << endl;
}cpp
#include "Song.h"
int main() {
Song a("Hello Viet Nam", 263);
Song b;
a.show();
b.show();
return 0;
}sh
g++ -std=c++17 -Wall Song.cpp main.cpp -o main.exetext
Hello Viet Nam (4 phut 23 giay)
Bai hat moi (0 phut 0 giay)- File
.h: chốt chặn#ifndef/#define/#endif,#include <string>, dùngstd::string, không viếtusing namespace std;. - File
.cpp:#include "Song.h"; mỗi hàm cóSong::trước tên.Song::Songlà constructor,void Song::show() constlà phương thức. - Lệnh
g++liệt kê mọi file.cpp, không liệt kê file.h.
6. Bốn lỗi hay gặp khi chia file
Quên ; sau } của lớp. Lỗi này hay báo ở chỗ rất lạ. Song.cpp include Song.h ngay trước <iostream>, nên g++ báo một loạt lỗi bên trong thư viện chuẩn trước, rồi mới tới dòng thật sự sai. Bỏ qua các dòng có đường dẫn kiểu C:/msys64/.../include/c++/..., tìm dòng nhắc tới file của bạn (trích, đường dẫn tuỳ máy):
text
C:/msys64/ucrt64/include/c++/15.2.0/exception:43:8: error: expected unqualified-id before string constant
43 | extern "C++" {
| ^~~~~
...
In file included from main.cpp:1:
Song.h:15:2: error: expected ';' after class definition
15 | }
| ^
| ;Quên Song:: trước tên hàm trong .cpp. g++ tưởng đó là một hàm thường, không thuộc lớp nào, nên không thấy các thuộc tính (trích):
text
Song.cpp:10:13: error: non-member function 'void show()' cannot have cv-qualifier
10 | void show() const {
| ^~~~~
Song.cpp: In function 'void show()':
Song.cpp:11:13: error: 'title' was not declared in this scopeQuên đưa Song.cpp vào lệnh g++. Phần biên dịch vẫn qua, nhưng bước liên kết không tìm thấy thân hàm (trích, đường dẫn đã rút gọn):
text
ld.exe: ...main.cpp:(.text+0x4c): undefined reference to `Song::Song(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, int)'
ld.exe: ...main.cpp:(.text+0x74): undefined reference to `Song::Song()'
ld.exe: ...main.cpp:(.text+0x80): undefined reference to `Song::show() const'
collect2.exe: error: ld returned 1 exit statusThiếu chốt chặn trong .h. Không có #ifndef / #define / #endif, chỉ cần file .h bị include hai lần trong cùng một file .cpp là g++ báo lớp bị định nghĩa lại:
text
In file included from main.cpp:2:
Song.h:4:7: error: redefinition of 'class Song'
4 | class Song {
| ^~~~
In file included from main.cpp:1:
Song.h:4:7: note: previous definition of 'class Song'
4 | class Song {
| ^~~~7. Vẽ UML từ mô tả bằng lời
Bài 4.2 đi ngược chiều: cho mô tả bằng lời, bạn vẽ sơ đồ trước rồi mới code. Ví dụ với mô tả sau:
Lớp
Classroom(phòng học) cần có: số phòng, toà nhà, số chỗ ngồi, số chỗ đã có người đặt (luôn bắt đầu bằng 0); một constructor mặc định và một constructor nhận số phòng, toà nhà, số chỗ ngồi; một hàm hiển thị.
- Gạch các danh từ chỉ dữ liệu, mỗi danh từ thành một thuộc tính. Đặt tên tiếng Anh ngắn gọn, rồi chọn kiểu: chữ thì
string, số đếm thìint. - Gạch các hành động, mỗi hành động thành một phương thức. Constructor trùng tên lớp và không có kiểu trả về; hàm hiển thị có kiểu
void. - Kiểm tra quyết định thiết kế. Thuộc tính nào luôn bắt đầu bằng một giá trị cố định thì không có mặt trong tham số constructor. Sơ đồ thể hiện điều đó ngay ở dòng constructor, và nên có thêm một ghi chú cho rõ.


Nhìn dòng + Classroom(room, building, seats) là biết booked không do người tạo đối tượng quyết định. Vì vậy trong code, constructor phải tự khởi tạo booked bằng 0 trong danh sách khởi tạo.
Vẽ bằng gì cũng được: vẽ trên giấy rồi chụp ảnh, hoặc dùng app.diagrams.net. Khung bài làm tuần 4 có sẵn file mau_so_do_lop.drawio là một hộp lớp trống: mở bằng File → Open from → Device, sửa chữ, rồi xuất ảnh bằng File → Export as → PNG.
Bài 4.1 — Dịch UML thành code (60 phút)
Mục tiêu 60 phút60:00Chưa bắt đầu
Sơ đồ UML của lớp Document:


Trong đó id là mã tài liệu, title là tựa đề, year là năm xuất bản. Ký hiệu + nghĩa là public. Tuần này để public hết cho dễ; tuần 5 sẽ đóng lại.
Nhiệm vụ:
- Viết
Document.hvàDocument.cppđúng theo UML trên. Document()— constructor mặc định — đặtid = "???",title = "Chua dat ten",year = 0.Document(id, title, year)— dùng danh sách khởi tạo, không gán trong thân hàm:cppDocument::Document(const string& id, const string& title, int year) : id(id), title(title), year(year) {}display()in một dòng theo đúng định dạng ở phần kết quả mong đợi.- Trong
main.cpp: tạo 2 tài liệu bằng constructor có tham số và 1 tài liệu bằng constructor mặc định, rồi gọidisplay()cho cả ba.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Tạo thư mục <MSSV>_w04, hoặc giải nén khung bài làm rồi đổi tên thư mục MSSV_w04.
Bước 2. Trong Document.h: chốt chặn (khung có sẵn), #include <string>, rồi khai báo lớp Document với public:. Nhớ dấu ; sau dấu }.
Bước 3. Dịch tầng 2 của sơ đồ thành ba dòng thuộc tính, theo đúng cách bảng ở mục 2 đã dịch + title : string và + seconds : int. Trong file .h viết std::string.
Bước 4. Dịch tầng 3: hai constructor (không có kiểu trả về) và display (kiểu void, thêm const). Chỉ viết nguyên mẫu, không viết thân hàm.
Bước 5. Trong Document.cpp: #include "Document.h", #include <iostream>, using namespace std;. Viết constructor mặc định bằng danh sách khởi tạo với ba giá trị đề cho, constructor có tham số theo mẫu trong đề, và display với tiền tố Document::.
Bước 6. Trong main.cpp: tạo hai tài liệu bằng constructor có tham số và một tài liệu bằng constructor mặc định (viết không có cặp ngoặc, mục 3), in dòng tiêu đề, rồi gọi display() cho cả ba.
Bước 7. Biên dịch cả ba file như lệnh trong đề. Khung bài làm có sẵn Reader.h và Reader.cpp chưa có code, nên biên dịch được ngay dù chưa làm bài 4.2. Nếu tự tạo thư mục mà chưa có Reader, tạm biên dịch hai file Document.cpp main.cpp và bỏ dòng #include "Reader.h".
Bước 8. So bốn dòng đầu với kết quả mong đợi, đủ từng dấu cách và dấu |.
Nếu g++ báo lỗi lạ bên trong <iostream> hoặc ở đường dẫn include/c++: xem lại dấu ; sau lớp (mục 6).
Nếu gặp request for member 'display' in 'd3', which is of non-class type 'Document()': bạn đã viết Document d3(); (mục 3).
Nếu gặp no declaration matches 'void Document::display()': thiếu const ở file .cpp (mục 4).
Bài 4.2 — Tự vẽ UML rồi mới code (40 phút)
Mục tiêu 40 phút40:00Chưa bắt đầu
Lần này làm ngược lại: vẽ UML trước, code sau.
Lớp Reader (bạn đọc) cần có:
- mã thẻ, họ tên, số tài liệu đang mượn (khởi tạo bằng 0);
- một constructor mặc định (mã thẻ và họ tên mặc định tuỳ bạn đặt) và một constructor nhận mã thẻ + họ tên;
- một hàm
display().
Nhiệm vụ:
- Vẽ sơ đồ UML của
Reader(vẽ tay chụp ảnh, hoặc draw.io — đều được). - Viết
Reader.h,Reader.cppđúng theo sơ đồ mình vừa vẽ. - Thêm vào
main.cpp: tạo một bạn đọc và gọidisplay().
Chú ý:
borrowingkhông nằm trong tham số của constructor — nó luôn bắt đầu từ 0. Đây là một quyết định thiết kế, và bạn phải thể hiện được nó trên sơ đồ UML.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Gạch các danh từ trong mô tả của đề: mã thẻ, họ tên, số tài liệu đang mượn. Đặt tên tiếng Anh cho từng thuộc tính (đề đã dùng sẵn tên borrowing cho số tài liệu đang mượn) và chọn kiểu (mục 7).
Bước 2. Vẽ hộp ba tầng. Tầng 3 có hai constructor và display. Dòng constructor có tham số chỉ ghi hai tham số: đó là cách sơ đồ thể hiện borrowing không do người tạo đối tượng quyết định. Ghi chú thêm cạnh borrowing cho rõ.
Bước 3. Xuất sơ đồ thành uml_reader.png, hoặc chụp ảnh bản vẽ tay, rồi đặt vào thư mục bài làm.
Bước 4. Viết Reader.h bằng cách dịch chính sơ đồ bạn vừa vẽ, như cách làm bài 4.1. Thang điểm dành 2 điểm cho việc sơ đồ và code khớp nhau, nên đừng sửa code mà quên sửa sơ đồ.
Bước 5. Viết Reader.cpp. Constructor có tham số phải khởi tạo cả borrowing bằng 0 trong danh sách khởi tạo, dù borrowing không phải tham số.
Bước 6. Thêm vào main.cpp: tạo một bạn đọc, in dòng tiêu đề, gọi display(). Biên dịch đủ ba file .cpp.
Bước 7. So dòng cuối với kết quả mong đợi: Dang muon: 0 tai lieu.
Nếu số đang mượn in ra một số lạ thay vì 0, bạn đã quên khởi tạo borrowing. Thuộc tính kiểu int không được khởi tạo sẽ mang giá trị rác, và g++ không cảnh báo gì. Một lần chạy thật với lỗi này:
text
--- BAN DOC ---
[BD] BD001 | Nguyen Van An | Dang muon: 3 tai lieuSố rác trên máy bạn có thể khác, thậm chí có lúc tình cờ ra 0. Vì vậy phải khởi tạo, đừng trông vào may mắn.
Nếu gặp undefined reference nhắc tới Reader::Reader: lệnh g++ đang thiếu Reader.cpp (mục 6).
Biên dịch
sh
g++ -std=c++17 -Wall Document.cpp Reader.cpp main.cpp -o main.exeMáy chưa cài được công cụ thì dùng tạm trình biên dịch online theo Phụ lục E.7 — nhớ tải code về máy trước khi đóng tab. Bài chấm điểm tuần 8 làm không có mạng, nên trước tuần 8 vẫn phải cài một công cụ trên máy.
Nếu quên một file .cpp bạn sẽ gặp lỗi kiểu undefined reference to Document::display(). Đó là lỗi liên kết, không phải lỗi cú pháp — hãy nhận ra nó, tuần nào cũng gặp lại.
Kết quả mong đợi
text
--- DANH MUC TAI LIEU ---
[TL] TL001 | Lap trinh huong doi tuong | 2019
[TL] TL002 | Vat ly dai cuong 1 | 2021
[TL] ??? | Chua dat ten | 0
--- BAN DOC ---
[BD] BD001 | Nguyen Van An | Dang muon: 0 tai lieuThư mục bài làm — giữ trên máy, không nộp
text
<MSSV>_w04/
Document.h Document.cpp
Reader.h Reader.cpp
main.cpp
uml_reader.png (hoặc .jpg — ảnh sơ đồ bạn vẽ ở bài 4.2)Bài tại lớp không nộp. Tuần 4 chỉ nộp bài về nhà ở cuối trang. Giữ nguyên thư mục này: tuần 5 làm tiếp trên chính nó, và các tuần sau cũng vậy cho tới cuối học kỳ.
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 được | 2 |
Có đủ file .h và .cpp cho cả hai lớp, đặt tên đúng | 2 |
Nội dung .h đầy đủ (có #ifndef, khai báo đúng UML) | 1 |
Nội dung .cpp đầy đủ, dùng danh sách khởi tạo | 2 |
| Constructor mặc định hoạt động đúng | 1 |
| UML bài 4.2 khớp với code đã viết | 2 |
Thang điểm này giống hệt thang điểm đề thi thực hành cuối kỳ. Bài thi và hai bài lấy điểm (tuần 5, tuần 8) đều chấm theo khung đó, nên tự chấm theo đây là tập đúng cách.
Bài về nhà — phần duy nhất phải nộp
Thêm lớp Librarian (thủ thư): mã nhân viên, họ tên, ca trực ("Sang" / "Chieu"), một constructor mặc định, một constructor ba tham số dùng danh sách khởi tạo, và display(). Vẽ UML trước rồi mới viết code, như bài 4.2.
Nộp thẳng trên Moodle, trong hoạt động Phòng lập trình ảo: gõ code vào đó rồi bấm Đánh giá, máy chạy bốn bộ kiểm thử và chấm điểm ngay; sửa rồi nộp lại bao nhiêu lần cũng được. Hạn nộp: trước buổi lab tuần 5 của lớp bạn — lớp học thứ hai: 23:59 chủ nhật 11/10; lớp học thứ tư: 23:59 thứ ba 13/10. Ngày giờ chính xác luôn hiện ngay trong hoạt động trên Moodle.
Nộp ba thứ: Librarian.h, Librarian.cpp và ảnh sơ đồ UML bạn vẽ (uml_librarian.png hoặc .jpg, tải lên bằng nút Nhập dữ liệu trên thanh công cụ). Không nộp main.cpp — máy chấm có main.cpp riêng, nó tạo các thủ thư rồi gọi display(), nên bạn chỉ cần viết lớp cho đúng.
Máy chấm so output từng ký tự, nên display() phải in đúng một dòng theo mẫu này:
text
[TT] NV001 | Tran Thi Mai | Ca truc: SangConstructor mặc định đặt mã là ???, họ tên là Chua dat ten, ca trực là Sang, tức là in ra:
text
[TT] ??? | Chua dat ten | Ca truc: SangHai lỗi làm chương trình không biên dịch được (0 điểm, nhưng sửa rồi nộp lại được): thiếu constructor mặc định, và display() thiếu const ở cuối — máy chấm in qua tham chiếu const nên bắt buộc phải có.
Thang điểm bài về nhà
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 |
|---|---|
| Chỉ constructor mặc định | 2,5 |
| Hai thủ thư, ca Sang và ca Chieu | 2,5 |
| Họ tên dài, mã khác | 2,5 |
| Ba thủ thư liên tiếp | 2,5 |
Sơ đồ UML là điểm cộng, tối đa 2 điểm, cộng thêm ngoài 10 điểm ở trên:
| Sơ đồ UML | Điểm cộng |
|---|---|
Khớp với chính code bạn nộp: đúng tên thuộc tính, đúng ký hiệu +/-, đúng tên lớp | +2 |
Có sơ đồ nhưng lệch tên hoặc lệch +/- so với code | +1 |
| Không nộp sơ đồ, hoặc nộp file không mở ra được sơ đồ | 0 |
Đáp án tuần này
Nộp bài xong rồi mở trang đáp án tuần 4 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.