Skip to content

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ỳ ​

  1. 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.
  2. Biên dịch bằng dòng lệnh, không bấm nút IDE:
    sh
    g++ -std=c++17 -Wall *.cpp -o main.exe
    Riê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ó main), 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.
  3. 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 đã.
  4. 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.
  5. 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.zip rồ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.

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

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 status

Lệ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 1

Chạ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 = 7

Hai điều đọc ra được:

  • Hàm nhận bản sao của soLuot (truyền tham trị). Sửa x trong hàm không làm soLuot đổi theo. Muốn hàm đưa kết quả ra ngoài, dùng return rồ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 main nó chưa gặp hàm nào tên nhanDoi:
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ếtNghĩa
int* pkhai báo p là con trỏ tới int
&soTrangđịa chỉ của biến soTrang
*pgiá 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 = 8

Nơ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 42
  • new double[n] xin một vùng đủ cho n phầ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ằng delete[]. Viết delete thiếu [] thì g++ với -Wall cả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) = 7

Nhờ 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ượtSo cặp vị tríSai thứ tự?Mảng sau bước này
10 và 1có, đổi1 5 4 2
11 và 2có, đổi1 4 5 2
12 và 3có, đổi1 4 2 5
20 và 1không1 4 2 5
21 và 2có, đổi1 2 4 5
30 và 1không1 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:

traiphaigiuaGiá trị ở giuaSo với 30
06321nhỏ hơn: bỏ nửa trái, trai = 4
46542lớn hơn: bỏ nửa phải, phai = 4
44430bằng: tìm thấy ở vị trí 4

Tìm 10:

traiphaigiuaGiá trị ở giuaSo với 10
06321lớn hơn: phai = 2
0218nhỏ hơn: trai = 2
22215lớn hơn: phai = 1
21trai > 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);

#endif

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

Trong 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 status

main.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: 19
Hướ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: 0

Nế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ằng delete[].
  • Bên trong tongTrang phải truy cập bằng con trỏ: *(a + i), không dùng a[i].
text
So cuon sach: 3
Nhap so trang tung cuon: 350 220 480
Tong so trang la 1050
Hướ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 hai int*. g++ báo lỗi invalid 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ên swap và bạn gọi swap(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::swap củ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ên hoanDoi.

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 947
Hướ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 3
text
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ậpIn ra
305Tim thay ma 305 o vi tri 3
400Khong 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.cpp

Sá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ảng2
Mảng chỉ có một phần tử2
Nhập 0 ngay, không tra cứu lần nào2
Mảng đã sắp sẵn, tìm phần tử ở giữa2

Đá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.

© 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