Giao diện
Bài lab · Tuần 3
Tuần 3 — Nền tảng C++ trước khi vào lớp và đối tượng
Hệ thống xuyên suốt học kỳ: THƯ VIỆN. Từ tuần này đến tuần 10 chúng ta xây một hệ thống mượn trả thư viện. Mỗi tuần thêm một phần vào chính code của bạn, không làm lại từ đầu. Giữ kỹ thư mục bài làm mỗi tuần.
Tuần này chưa có lớp và đối tượng. Ta làm quen với dữ liệu của thư viện ở dạng thô: tên bạn đọc, số lượt mượn, số trang, mã tài liệu.
Quy ước áp dụng cho cả học kỳ
- Output không dấu tiếng Việt.
Da cho muon, không phảiĐã cho mượn. Console Windows hay vỡ font và bạn sẽ mất thời gian vô ích trong phòng thi. - Biên dịch bằng dòng lệnh, không bấm nút IDE:shRiêng tuần 3, mỗi bài từ 3.1 đến 3.6 là một chương trình riêng (file nào cũng có
g++ -std=c++17 -Wall *.cpp -o main.exemain), nên biên dịch từng file, ví dụg++ -std=c++17 -Wall 3_1_the_ban_doc.cpp -o 3_1.exe. - Nộp bài trước khi xem đáp án. Trang đáp án mỗi tuần chỉ mở bằng một mã giảng viên đăng trên Moodle sau hạn nộp. Nộp bài chưa chạy được vẫn được tính — mục đích là bạn phải tự làm trước đã.
- Output phải giống mẫu từng ký tự, kể cả khoảng trắng và dấu câu. Dòng nhắc nhập in không xuống dòng, ví dụ
cout << "So cuon sach: ";, để phần bạn gõ nằm ngay sau nó như trong mẫu. Lời nhắc cũng chỉ in đúng số lần như trong mẫu: bài 3.3 và 3.4 in lời nhắc một lần, trước vòng lặp, không in lại mỗi vòng. - Mỗi tuần chỉ nộp một thứ: bài về nhà, hạn trước buổi lab tuần sau. Tuần nào Moodle có hoạt động Phòng lập trình ảo thì gõ code thẳng trong đó rồi bấm Đánh giá; tuần nào không có thì nén thành
<MSSV>_wNN_bvn.ziprồi nộp lên Moodle. Bài làm tại lớp không nộp, nhưng phải giữ lại trên máy: từ tuần 4 trở đi tuần sau làm tiếp trên chính thư mục đó. Riêng tuần 8 làm bài tại lớp và nộp ngay trong buổi.
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
Mỗi mục dưới đây là một mảnh kiến thức mà ít nhất một bài tuần này cần tới. Ví dụ nào cũng đã biên dịch và chạy thật, và cố ý không trùng với bài lab: đọc để hiểu cách viết, rồi tự viết bài của bạn.
1. Biên dịch và chạy từng file
Tuần 3, mỗi bài từ 3.1 đến 3.6 là một chương trình riêng, nên biên dịch từng file:
sh
g++ -std=c++17 -Wall 3_1_the_ban_doc.cpp -o 3_1.exe-std=c++17: dịch theo chuẩn C++17, chuẩn môn học dùng.-Wall: bật cảnh báo. Có cảnh báo vẫn ra file chạy được, nhưng cảnh báo thường là dấu hiệu của một lỗi.-o 3_1.exe: đặt tên file chạy. Không ghi thì g++ đặt tên làa.exe.
Chạy bằng 3_1.exe trong Command Prompt, hoặc .\3_1.exe trong PowerShell (terminal mặc định của VS Code).
Đừng gõ *.cpp trong thư mục có sáu bài này. Sáu hàm main đụng nhau, và g++ in năm dòng như dòng dưới đây rồi dừng ở bước liên kết (đường dẫn đầu dòng đã rút gọn):
text
ld.exe: ...3_2_doan_ma_tai_lieu.cpp:(.text+0x0): multiple definition of `main'; ...3_1_the_ban_doc.cpp:(.text+0x0): first defined here
collect2.exe: error: ld returned 1 exit statusLệnh *.cpp chỉ dùng cho thư mục có đúng một hàm main, như 3_0_hello của bài 3.0 và các bài từ tuần 4 trở đi.
Chưa cài xong công cụ, phải dùng tạm trình biên dịch online? Xem Phụ lục E.7. Trình đó cũng dịch mọi file .cpp đang mở, nên mỗi lúc chỉ để một bài trong dự án: làm xong bài nào thì tải file về máy rồi xoá tab đó, đừng mở cả sáu bài cùng lúc. Tải lại trang là mất code, và tuần 8 cùng phòng thi đều không có mạng, nên vẫn phải cài công cụ trên máy trước buổi lab tuần 4 (Phụ lục E).
2. Nhập chuỗi có khoảng trắng: cin >> và getline
cin >> dừng đọc ở khoảng trắng đầu tiên. Muốn lấy cả dòng, dùng getline(cin, bien).
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
string tua;
cout << "Tua sach: ";
cin >> tua;
cout << "Da doc duoc: [" << tua << "]" << endl;
return 0;
}text
Tua sach: Lap trinh huong doi tuong
Da doc duoc: [Lap]Đổi dòng cin >> tua; thành getline(cin, tua); thì chương trình đọc được cả dòng:
text
Tua sach: Lap trinh huong doi tuong
Da doc duoc: [Lap trinh huong doi tuong]3. Vòng lặp do … while
Thân vòng lặp chạy ít nhất một lần, rồi mới kiểm tra điều kiện ở cuối. Hợp với kiểu “hỏi cho tới khi được”.
cpp
#include <iostream>
using namespace std;
int main() {
int soTrang;
do {
cout << "So trang (phai lon hon 0): ";
cin >> soTrang;
} while (soTrang <= 0);
cout << "Da ghi nhan sach " << soTrang << " trang." << endl;
return 0;
}text
So trang (phai lon hon 0): -5
So trang (phai lon hon 0): 0
So trang (phai lon hon 0): 320
Da ghi nhan sach 320 trang.4. Số ngẫu nhiên
rand() trả về một số nguyên ngẫu nhiên. rand() % 6 cho số từ 0 đến 5, cộng thêm 1 thành từ 1 đến 6. srand((unsigned) time(NULL)) lấy giờ hiện tại làm “hạt giống”, để mỗi lần chạy ra một dãy khác. Chỉ gọi một lần, ở đầu chương trình.
cpp
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
int main() {
srand((unsigned) time(NULL)); // goi MOT lan, o dau chuong trinh
cout << "Tung xuc xac 5 lan:";
for (int i = 0; i < 5; i++)
cout << " " << rand() % 6 + 1; // so tu 1 den 6
cout << endl;
return 0;
}text
Tung xuc xac 5 lan: 2 6 3 4 1Chạy lại sẽ ra một dãy khác.
5. Hàm: nguyên mẫu và định nghĩa
Nguyên mẫu (khai báo) cho trình biên dịch biết hàm tên gì, nhận gì, trả về gì. Định nghĩa là phần có thân hàm. Đặt nguyên mẫu trên main thì định nghĩa để dưới main cũng được.
cpp
#include <iostream>
using namespace std;
void nhanDoi(int x); // nguyen mau: khai bao truoc main
int main() {
int soLuot = 7;
nhanDoi(soLuot);
cout << "Trong main: soLuot = " << soLuot << endl;
return 0;
}
void nhanDoi(int x) { // dinh nghia: dat sau main
x = x * 2;
cout << "Trong ham : x = " << x << endl;
}text
Trong ham : x = 14
Trong main: soLuot = 7Hai điều đọc ra được:
- Hàm nhận bản sao của
soLuot(truyền tham trị). Sửaxtrong hàm không làmsoLuotđổi theo. Muốn hàm đưa kết quả ra ngoài, dùngreturnrồi gán lại ở nơi gọi. - Xoá dòng nguyên mẫu đi thì g++ báo lỗi ngay chỗ gọi, vì lúc đọc tới
mainnó chưa gặp hàm nào tênnhanDoi:
text
main.cpp: In function 'int main()':
main.cpp:6:5: error: 'nhanDoi' was not declared in this scope
6 | nhanDoi(soLuot);
| ^~~~~~~6. Con trỏ: & lấy địa chỉ, * lấy giá trị tại địa chỉ
Mỗi biến nằm ở một địa chỉ trong bộ nhớ. Con trỏ int* là biến dùng để giữ địa chỉ của một biến int.
cpp
#include <iostream>
using namespace std;
int main() {
int soTrang = 350;
int* p = &soTrang; // p giu DIA CHI cua soTrang
cout << "soTrang = " << soTrang << endl;
cout << "p = " << p << endl;
cout << "*p = " << *p << endl;
*p = 400; // sua gia tri TAI dia chi p
cout << "Sau *p = 400, soTrang = " << soTrang << endl;
return 0;
}text
soTrang = 350
p = 0x23867ffbd4
*p = 350
Sau *p = 400, soTrang = 400Địa chỉ in ra đổi theo từng lần chạy. Điều cần nhớ: *p = 400 ghi thẳng vào ô nhớ của soTrang.
| Viết | Nghĩa |
|---|---|
int* p | khai báo p là con trỏ tới int |
&soTrang | địa chỉ của biến soTrang |
*p | giá trị nằm tại địa chỉ mà p giữ, đọc hay ghi đều được |
7. Truyền con trỏ vào hàm
Mục 5 cho thấy hàm chỉ nhận bản sao. Muốn hàm sửa được biến của nơi gọi, truyền địa chỉ của biến đó:
cpp
#include <iostream>
using namespace std;
void tangMot(int* x) {
*x = *x + 1; // sua bien cua noi goi, thong qua dia chi
}
int main() {
int soLuot = 7;
tangMot(&soLuot); // truyen DIA CHI cua soLuot
cout << "soLuot = " << soLuot << endl;
return 0;
}text
soLuot = 8Nơi gọi viết &soLuot; trong hàm dùng *x để đọc và ghi vào đúng biến đó.
8. Mảng động: new[] và delete[]
Mảng thường như int a[100] phải biết kích thước từ lúc viết code. Khi kích thước chỉ biết lúc chương trình chạy, chẳng hạn do người dùng nhập, xin bộ nhớ bằng new:
cpp
#include <iostream>
using namespace std;
int main() {
int n;
cout << "So mat hang: ";
cin >> n;
double* gia = new double[n]; // xin n o nho luc chuong trinh dang chay
for (int i = 0; i < n; i++)
gia[i] = 10.5 * (i + 1);
cout << "Mat hang cuoi cung gia " << gia[n - 1] << endl;
delete[] gia; // tra lai ca vung nho; co [] vi da xin bang new[]
return 0;
}text
So mat hang: 4
Mat hang cuoi cung gia 42new double[n]xin một vùng đủ chonphần tử và trả về địa chỉ phần tử đầu. Sau đó dùng như mảng thường.- Xin bằng
new[]thì trả bằngdelete[]. Viếtdeletethiếu[]thì g++ với-Wallcảnh báo[-Wmismatched-new-delete].
9. Tên mảng là địa chỉ: a[i] và *(a + i)
Tên mảng, hay một con trỏ tới phần tử đầu, cộng thêm i là địa chỉ của phần tử thứ i. Vì vậy a[i] và *(a + i) là một:
cpp
#include <iostream>
using namespace std;
int main() {
int ma[] = {12, 40, 7};
int* p = ma; // ten mang chinh la dia chi phan tu dau tien
for (int i = 0; i < 3; i++)
cout << "ma[" << i << "] = " << ma[i] << " *(p + " << i << ") = " << *(p + i) << endl;
return 0;
}text
ma[0] = 12 *(p + 0) = 12
ma[1] = 40 *(p + 1) = 40
ma[2] = 7 *(p + 2) = 7Nhờ vậy, một hàm chỉ cần nhận con trỏ tới phần tử đầu cùng số phần tử là đọc được cả mảng.
10. Bubble Sort, lần theo bằng tay
Ý tưởng: đi từ đầu mảng, so từng cặp kề nhau; cặp nào sai thứ tự thì đổi chỗ. Hết một lượt, số lớn nhất đã “nổi” về cuối, nên lượt sau không cần xét nó nữa. Với mảng {5, 1, 4, 2}:
| Lượt | So cặp vị trí | Sai thứ tự? | Mảng sau bước này |
|---|---|---|---|
| 1 | 0 và 1 | có, đổi | 1 5 4 2 |
| 1 | 1 và 2 | có, đổi | 1 4 5 2 |
| 1 | 2 và 3 | có, đổi | 1 4 2 5 |
| 2 | 0 và 1 | không | 1 4 2 5 |
| 2 | 1 và 2 | có, đổi | 1 2 4 5 |
| 3 | 0 và 1 | không | 1 2 4 5 |
Mảng 4 phần tử cần 3 lượt: lượt thứ nhất xét 3 cặp, lượt thứ hai 2 cặp, lượt thứ ba 1 cặp.
11. Tìm kiếm nhị phân, lần theo bằng tay
Chỉ dùng được trên mảng đã sắp xếp. Giữ một đoạn [trai, phai] vẫn còn có thể chứa số cần tìm; mỗi bước nhìn phần tử ở giữa đoạn rồi bỏ đi một nửa. Mảng {3, 8, 15, 21, 30, 42, 57}, vị trí từ 0 đến 6.
Tìm 30:
| trai | phai | giua | Giá trị ở giua | So với 30 |
|---|---|---|---|---|
| 0 | 6 | 3 | 21 | nhỏ hơn: bỏ nửa trái, trai = 4 |
| 4 | 6 | 5 | 42 | lớn hơn: bỏ nửa phải, phai = 4 |
| 4 | 4 | 4 | 30 | bằng: tìm thấy ở vị trí 4 |
Tìm 10:
| trai | phai | giua | Giá trị ở giua | So với 10 |
|---|---|---|---|---|
| 0 | 6 | 3 | 21 | lớn hơn: phai = 2 |
| 0 | 2 | 1 | 8 | nhỏ hơn: trai = 2 |
| 2 | 2 | 2 | 15 | lớn hơn: phai = 1 |
| 2 | 1 | trai > phai: đoạn rỗng, không có, trả về -1 |
Để ý dòng thứ ba của cả hai bảng: khi đoạn chỉ còn một phần tử (trai bằng phai) vẫn phải xét. Đó là lý do điều kiện lặp là trai <= phai. Bảy phần tử cần nhìn nhiều nhất 3 lần; một triệu phần tử cũng chỉ cần khoảng 20 lần.
Bài 3.0 — Hello world chia ba file (10 phút)
Mục tiêu 10 phút10:00Chưa bắt đầu
Bài mẫu, không chấm, không nộp. Mục đích: thấy tận mắt một chương trình C++ chia thành file .h (khai báo), file .cpp (định nghĩa) và main.cpp (dùng). Từ tuần 4, mọi bài đều viết theo đúng khuôn này, chỉ khác là trong file .h sẽ là một lớp thay vì một hàm.
LoiChao.h — chỉ khai báo: có hàm nào, nhận gì, trả về gì.
cpp
// LoiChao.h — KHAI BAO: cho biet "co nhung ham nao, nhan gi, tra ve gi". Khong viet than ham o day.
#ifndef LOICHAO_H
#define LOICHAO_H
#include <string>
void chaoBanDoc(const std::string& ten);
#endifLoiChao.cpp — định nghĩa: viết thân của hàm đã khai báo.
cpp
// LoiChao.cpp — DINH NGHIA: viet than cua cac ham da khai bao trong LoiChao.h.
#include <iostream>
#include "LoiChao.h"
void chaoBanDoc(const std::string& ten) {
std::cout << "Xin chao " << ten << "! Chuc ban hoc tot mon OOP." << std::endl;
}main.cpp — dùng: chỉ cần #include "LoiChao.h" là gọi được hàm.
cpp
// main.cpp — DUNG: chi can #include "LoiChao.h" la goi duoc ham, khong can biet than ham nam o dau.
#include "LoiChao.h"
int main() {
chaoBanDoc("Nguyen Van An");
return 0;
}Đặt ba file vào cùng một thư mục 3_0_hello, biên dịch cả ba rồi chạy:
sh
g++ -std=c++17 -Wall *.cpp -o main.exe
main.exeTrong PowerShell (terminal mặc định của VS Code), gõ .\main.exe thay cho main.exe.
text
Xin chao Nguyen Van An! Chuc ban hoc tot mon OOP.Nhiệm vụ: thêm hàm void tamBiet(const std::string& ten); in ra Tam biet <ten>, hen gap lai!. Khai báo trong LoiChao.h, định nghĩa trong LoiChao.cpp, gọi trong main ngay sau lời chào.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Tạo thư mục 3_0_hello, hoặc mở thư mục có sẵn trong khung bài làm. Nếu tự tạo, gõ lại ba file như trên đề thay vì chép dán: gõ một lần là nhớ file nào chứa gì.
Bước 2. Mở terminal ngay trong thư mục đó, biên dịch cả ba file bằng *.cpp rồi chạy (mục 1 phần lý thuyết).
Bước 3. Làm nhiệm vụ theo đúng thứ tự: thêm dòng khai báo tamBiet vào LoiChao.h, viết thân hàm trong LoiChao.cpp, rồi gọi trong main.cpp. Biên dịch lại cả ba file.
Nếu quên đưa LoiChao.cpp vào lệnh biên dịch, ví dụ chỉ gõ g++ -std=c++17 -Wall main.cpp -o main.exe, g++ báo lỗi ở bước liên kết (đường dẫn đầu dòng đã rút gọn):
text
ld.exe: ...main.cpp:(.text+0x3c): undefined reference to `chaoBanDoc(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
collect2.exe: error: ld returned 1 exit statusmain.cpp biết hàm chaoBanDoc có tồn tại nhờ file .h, nên phần biên dịch vẫn qua. Nhưng lúc ghép các phần lại, g++ không tìm thấy thân hàm, vì thân hàm nằm trong LoiChao.cpp mà lệnh không có. Gặp chữ undefined reference, việc đầu tiên là kiểm tra lệnh biên dịch đã đủ các file .cpp chưa. Khối trên là lúc chưa làm nhiệm vụ; sau khi thêm tamBiet, lệnh thiếu file in hai dòng undefined reference, mỗi hàm một dòng.
Để ý ba dòng #ifndef, #define, #endif trong LoiChao.h. Đó là header guard: nếu file .h bị #include hai lần trong cùng một file .cpp, phần bên trong chỉ được đọc một lần. Từ tuần 4, file .h nào bạn viết cũng phải có ba dòng này, với một tên riêng cho từng file.
Bài 3.1 — Thẻ bạn đọc (10 phút)
Mục tiêu 10 phút10:00Chưa bắt đầu
Nhập họ tên bạn đọc, in ra thẻ mượn.
Yêu cầu: dùng getline để nhận cả họ và tên có khoảng trắng.
text
Nhap ho ten ban doc: Nguyen Van An
--- THE MUON THU VIEN ---
Chao Nguyen Van An, the cua ban da san sang!Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Mở 3_1_the_ban_doc.cpp trong khung bài làm, hoặc tạo file mới có #include <iostream>, #include <string> và using namespace std;.
Bước 2. Trong main, khai báo một biến kiểu string để giữ họ tên.
Bước 3. In lời nhắc, rồi đọc cả dòng vào biến đó (mục 2 phần lý thuyết).
Bước 4. In hai dòng của thẻ. So từng ký tự với kết quả mẫu trên đề: ba dấu gạch mỗi bên, dấu phẩy, dấu chấm than.
Bước 5. Biên dịch, chạy, gõ Nguyen Van An.
Nếu thẻ in ra thế này thì bạn đang đọc bằng cin >>, nên chỉ lấy được chữ đầu tiên:
text
Nhap ho ten ban doc: Nguyen Van An
--- THE MUON THU VIEN ---
Chao Nguyen, the cua ban da san sang!Bài 3.2 — Đoán mã tài liệu (20 phút)
Mục tiêu 20 phút20:00Chưa bắt đầu
Hệ thống giữ một mã tài liệu ngẫu nhiên từ 1 đến 100. Người dùng đoán cho tới khi đúng.
Yêu cầu:
- Đoán nhỏ hơn → in
Ma that lon hon so do. - Đoán lớn hơn → in
Ma that nho hon so do. - Đoán đúng → in
Chinh xac! Ban tim ra sau <n> lan doan. - Đếm số lần đoán.
Gợi ý: srand((unsigned) time(NULL)); rồi rand() % 100 + 1. Dùng do ... while.
text
He thong dang giu 1 ma tai lieu tu 1 den 100. Ban doan thu?
Ma ban doan: 50
Ma that nho hon so do.
Ma ban doan: 25
Ma that lon hon so do.
Ma ban doan: 37
Chinh xac! Ban tim ra sau 3 lan doan.Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Có #include <cstdlib> (cho rand, srand) và #include <ctime> (cho time). Khung bài làm có sẵn.
Bước 2. Đầu main: gọi srand một lần, rồi tạo mã bí mật trong khoảng 1 đến 100 (mục 4). Mã này tạo trước vòng lặp.
Bước 3. Khai báo biến giữ số người dùng đoán và biến đếm số lần; biến đếm bắt đầu từ 0. In câu mở đầu.
Bước 4. Viết do { ... } while (...); (mục 3). Trong thân vòng lặp: in lời nhắc, nhập số đoán, tăng biến đếm, rồi dùng if / else if / else để in đúng một trong ba câu của đề.
Bước 5. Điều kiện lặp: còn lặp khi số đoán khác mã bí mật.
Bước 6. Lúc thử, có thể tạm in mã bí mật ra để kiểm tra đủ các nhánh. Xoá dòng đó trước khi nộp.
Mẹo chơi, cũng là ý tưởng của bài 3.6: mỗi lần đoán số ở giữa khoảng còn lại. Như vậy không bao giờ quá 7 lần, vì 2⁷ = 128 lớn hơn 100. Một lần chạy thật, mã bí mật là 55:
text
He thong dang giu 1 ma tai lieu tu 1 den 100. Ban doan thu?
Ma ban doan: 50
Ma that lon hon so do.
Ma ban doan: 75
Ma that nho hon so do.
Ma ban doan: 62
Ma that nho hon so do.
Ma ban doan: 56
Ma that nho hon so do.
Ma ban doan: 53
Ma that lon hon so do.
Ma ban doan: 54
Ma that lon hon so do.
Ma ban doan: 55
Chinh xac! Ban tim ra sau 7 lan doan.Nếu mã bí mật đổi sau mỗi lần đoán thì bạn đang tạo mã bên trong vòng lặp. Đưa dòng đó lên trước do.
Nếu chương trình in mãi không dừng thì bạn vừa gõ chữ thay vì số: cin >> doan thất bại, doan thành 0 và không đọc thêm được gì nữa, nên vòng lặp in mãi Ma ban doan: Ma that lon hon so do.. Bấm Ctrl + C để dừng, chạy lại rồi gõ số. Bài này chưa cần xử lý trường hợp người dùng gõ sai kiểu.
Bài 3.3 — Thống kê lượt mượn bằng hàm (20 phút)
Mục tiêu 20 phút20:00Chưa bắt đầu
Nhập số ngày n, nhập số lượt mượn từng ngày, cộng dồn qua một hàm riêng.
Yêu cầu bắt buộc: phải có hàm int themLuot(int tong, int them); khai báo trước main, định nghĩa sau main. Không được cộng thẳng trong main.
text
So ngay can thong ke: 3
Nhap so luot muon moi ngay: 5 8 6
Tong so luot muon: 19Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Dòng nguyên mẫu int themLuot(int tong, int them); đặt trên main. Khung bài làm có sẵn.
Bước 2. Trong main: nhập n; khai báo biến tổng, gán bằng 0.
Bước 3. In lời nhắc Nhap so luot muon moi ngay: một lần, trước vòng lặp: trong mẫu chỉ có một lời nhắc cho cả ba số. Rồi cho vòng lặp chạy n lần. Mỗi lần đọc một số lượt, rồi cập nhật tổng bằng cách gán kết quả mà themLuot trả về lại cho biến tổng. Nhớ mục 5: hàm chỉ nhận bản sao, nó không tự sửa biến của bạn.
Bước 4. In tổng. Dưới main, viết định nghĩa themLuot: trả về tổng mới.
Bước 5. Chạy, nhập 3, rồi nhập cả ba số trên một dòng 5 8 6. cin >> đọc lần lượt từng số, cách nhau bằng khoảng trắng hay Enter đều được.
Nếu tổng in ra bằng 0 thì bạn đã gọi hàm nhưng quên gán kết quả lại cho biến tổng:
text
So ngay can thong ke: 3
Nhap so luot muon moi ngay: 5 8 6
Tong so luot muon: 0Nếu g++ báo 'themLuot' was not declared in this scope thì bạn thiếu dòng nguyên mẫu trên main, giống ví dụ ở mục 5.
Nếu lời nhắc Nhap so luot muon moi ngay: in ra ba lần thì dòng cout đó đang nằm trong vòng lặp. Đưa nó ra trước vòng lặp: máy chấm so output từng ký tự, in thừa lời nhắc là mất điểm phần chạy đúng.
Bài 3.4 — Con trỏ và mảng động (20 phút)
Mục tiêu 20 phút20:00Chưa bắt đầu
Nhập số cuốn sách n, cấp phát động mảng int gồm n phần tử để chứa số trang, tính tổng số trang bằng hàm int tongTrang(int* a, int n);.
Yêu cầu bắt buộc:
- Cấp phát bằng
new int[n], giải phóng bằngdelete[]. - Bên trong
tongTrangphải truy cập bằng con trỏ:*(a + i), không dùnga[i].
text
So cuon sach: 3
Nhap so trang tung cuon: 350 220 480
Tong so trang la 1050Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Nhập số cuốn sách n.
Bước 2. Khai báo một con trỏ kiểu int* và cho nó trỏ tới vùng nhớ xin bằng new int[n] (mục 8).
Bước 3. In lời nhắc Nhap so trang tung cuon: một lần, trước vòng lặp, giống bài 3.3. Rồi cho vòng lặp nhập n số trang vào vùng nhớ đó. Trong main, viết theo kiểu chỉ số [i] là được.
Bước 4. Gọi tongTrang, truyền con trỏ và n, in kết quả. Câu cuối của bài này không có dấu hai chấm: Tong so trang la 1050, khác bài 3.3 là Tong so luot muon: 19. So lại với mẫu trên đề trước khi nộp.
Bước 5. Trước return 0;, trả lại vùng nhớ bằng delete[].
Bước 6. Viết thân tongTrang: biến tổng bằng 0, vòng lặp i từ 0 tới n - 1, cộng dồn bằng *(a + i) (mục 9). Không dùng a[i] trong hàm này: đề bắt buộc.
Nếu g++ cảnh báo [-Wmismatched-new-delete] thì bạn viết delete thiếu []. Chương trình vẫn chạy, nhưng sai quy tắc cấp phát và mất điểm phần này.
Khai báo int a[n] rồi dùng luôn thì không phải cấp phát động, không đạt yêu cầu dù chương trình có thể vẫn chạy.
Bài 3.5 — Sắp xếp mã tài liệu, hoán đổi bằng con trỏ (20 phút)
Mục tiêu 20 phút20:00Chưa bắt đầu
Sắp xếp tăng dần mảng mã tài liệu bằng Bubble Sort.
Yêu cầu bắt buộc: phải có hàm void hoanDoi(int* a, int* b); và Bubble Sort phải gọi hàm này.
⚠️ Hai chỗ hay sai nhất của bài này.
1. Truyền giá trị thay vì địa chỉ.
hoanDoi(ma[j], ma[j + 1])truyền hai sốint, trong khi hàm cần haiint*. g++ báo lỗiinvalid conversion from 'int' to 'int*'và gợi ý luôn cách sửa là thêm&. Phải truyền địa chỉ:hoanDoi(&ma[j], &ma[j + 1]);2. Đặt tên hàm là
swap. Nếu hàm của bạn tênswapvà bạn gọiswap(ma[j], ma[j + 1]), chương trình vẫn biên dịch, vẫn sắp xếp đúng — nhưng thứ chạy làstd::swapcủa thư viện chuẩn (vì cóusing namespace std;), còn hàm con trỏ của bạn không bao giờ chạy. Cả bài tập thành vô nghĩa, nên đề bắt buộc tênhoanDoi.Cách tự kiểm tra: thêm
cout << "hoanDoi duoc goi" << endl;vào đầu hàm. Chạy mà không thấy dòng đó in ra lần nào là hàm của bạn chưa hề được gọi.
Dữ liệu khai báo sẵn trong main, không nhập từ bàn phím: int ma[] = {305, 118, 947, 220, 116, 673};
text
Truoc khi sap xep: 305 118 947 220 116 673
Sau khi sap xep : 116 118 220 305 673 947Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Viết thân hoanDoi: cần một biến tạm. Đọc giá trị tại địa chỉ bằng *a, *b rồi ghi chéo lại (mục 6, 7). Giống đổi nước giữa hai cốc: phải có cốc thứ ba.
Bước 2. Trong main: khai báo mảng với dữ liệu của đề, tính số phần tử bằng sizeof(ma) / sizeof(ma[0]).
Bước 3. In mảng trước khi sắp xếp, các số cách nhau một khoảng trắng.
Bước 4. Bubble Sort bằng hai vòng lặp lồng nhau, theo bảng ở mục 10: vòng ngoài đếm lượt, vòng trong so cặp vị trí j và j + 1; sai thứ tự thì gọi hoanDoi với địa chỉ của hai phần tử đó.
Bước 5. In mảng sau khi sắp xếp. Đề có hai khoảng trắng trước dấu hai chấm: Sau khi sap xep :.
Bước 6. Kiểm tra hàm có thật sự được gọi: tạm thêm một dòng cout vào đầu hoanDoi như đề gợi ý, chạy thử, rồi xoá dòng đó đi.
Nếu g++ báo lỗi dưới đây thì bạn đang truyền giá trị thay vì địa chỉ. Chính g++ đã gợi ý cách sửa: thêm &. Trích hai dòng chính, số dòng trong file của bạn sẽ khác:
text
3_5_sap_xep_ma.cpp:14:29: error: invalid conversion from 'int' to 'int*' [-fpermissive]
3_5_sap_xep_ma.cpp:14:29: note: possible fix: take the address with '&'Bài 3.6 — Tìm kiếm nhị phân (25 phút)
Mục tiêu 25 phút25:00Chưa bắt đầu
Dùng kết quả đã sắp xếp của bài 3.5 làm dữ liệu vào, khai báo sẵn trong main: int ma[] = {116, 118, 220, 305, 673, 947};. Nhập một mã, tìm bằng tìm kiếm nhị phân, trả về vị trí (tính từ 0, như chỉ số mảng) hoặc -1.
text
Ma tai lieu can tim: 305
Tim thay ma 305 o vi tri 3text
Ma tai lieu can tim: 400
Khong co ma 400 trong kho.Bài 3.5 → 3.6 là mắt xích đầu tiên của học kỳ: đầu ra của bài trước là đầu vào của bài sau. Từ tuần 4 trở đi mọi tuần đều như vậy.
Hướng dẫn từng bước — mở khi đã tự thử mà vẫn bí
Bước 1. Khai báo mảng đã sắp xếp (kết quả bài 3.5) và số phần tử.
Bước 2. Nhập mã cần tìm.
Bước 3. Viết hàm tìm theo đúng bảng ở mục 11: đoạn đang xét [trai, phai] lúc đầu là cả mảng; lặp khi đoạn còn phần tử; tính giua; bằng thì trả về vị trí; phần tử giữa nhỏ hơn số cần tìm thì bỏ nửa trái; lớn hơn thì bỏ nửa phải; ra khỏi vòng lặp mà vẫn chưa thấy thì trả về -1.
Bước 4. Trong main, gọi hàm và in một trong hai câu của đề, tuỳ kết quả có bằng -1 hay không.
Bước 5. Thử đủ bốn trường hợp. Kết quả đúng phải là:
| Nhập | In ra |
|---|---|
305 | Tim thay ma 305 o vi tri 3 |
400 | Khong co ma 400 trong kho. |
116 (đầu mảng) | Tim thay ma 116 o vi tri 0 |
947 (cuối mảng) | Tim thay ma 947 o vi tri 5 |
Nếu 947 hoặc 305 báo không có thì điều kiện lặp của bạn đang là trai < phai, nên đoạn chỉ còn một phần tử bị bỏ qua. Với điều kiện đó, cả hai lần chạy dưới đây đều sai:
text
Ma tai lieu can tim: 947
Khong co ma 947 trong kho.text
Ma tai lieu can tim: 305
Khong co ma 305 trong kho.Thư mục bài làm — giữ trên máy, không nộp
Sáu bài tại lớp, mỗi bài một file, đặt tên đúng để sau này dễ tìm:
text
<MSSV>_w03/
3_1_the_ban_doc.cpp
3_2_doan_ma_tai_lieu.cpp
3_3_tong_luot_muon.cpp
3_4_con_tro_tong_trang.cpp
3_5_sap_xep_ma.cpp
3_6_tim_nhi_phan.cppSáu file này không nộp. Tuần 3 chỉ nộp bài về nhà ở cuối trang. Giữ thư mục lại: bài 3.5 và 3.6 chính là nguyên liệu của bài về nhà.
Tự chấm bài tại lớp bằng cách so với kết quả mẫu ngay trên đề, từng ký tự một. Sau hạn nộp, mở trang đáp án bằng mã giảng viên đăng trên Moodle rồi so tiếp.
Bài về nhà — phần duy nhất phải nộp
Gộp 3.5 và 3.6 thành một chương trình: nhập n mã tài liệu → sắp xếp → in ra → cho phép tra cứu nhiều lần cho tới khi người dùng nhập 0 để thoát.
Nộp thẳng trên Moodle, trong hoạt động Phòng lập trình ảo ở mục Bài tập: gõ hoặc dán code vào trang đó rồi bấm Đánh giá. Máy chạy năm bộ kiểm thử và chấm điểm ngay; sai thì sửa rồi nộp lại, không giới hạn số lần. Không phải nén zip, không phải tải lên file. Hạn nộp: 23:59 thứ hai 5/10.
Máy chấm so output từng ký tự, nên lần này lời nhắc không được tự đặt. In đúng mẫu dưới đây:
text
So ma tai lieu: 6
Nhap cac ma: 305 118 947 220 116 673
Truoc khi sap xep: 305 118 947 220 116 673
Sau khi sap xep : 116 118 220 305 673 947
Ma can tim (0 de thoat): 305
Tim thay ma 305 o vi tri 3
Ma can tim (0 de thoat): 400
Khong co ma 400 trong kho.
Ma can tim (0 de thoat): 0
Da thoat tra cuu.Ba chỗ hay sai: dòng Sau khi sap xep có hai khoảng trắng trước dấu hai chấm, cho thẳng cột với dòng trên; vị trí tính từ 0; nhập 0 thì dừng vòng lặp và in Da thoat tra cuu.
Thang điểm bài về nhà
Mười điểm chia đều cho năm bộ kiểm thử, mỗi bộ 2 điểm. Bộ nào sai một ký tự là mất trọn 2 điểm của bộ đó.
| Bộ kiểm thử | Điểm |
|---|---|
| Ví dụ trên đề: 6 mã, một lần tìm thấy và một lần không có | 2 |
| Tìm phần tử đầu mảng và cuối mảng | 2 |
| Mảng chỉ có một phần tử | 2 |
Nhập 0 ngay, không tra cứu lần nào | 2 |
| Mảng đã sắp sẵn, tìm phần tử ở giữa | 2 |
Đáp án tuần này
Nộp bài xong rồi mở trang đáp án tuần 3 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.