Bài 39.5: Bài tập thực hành dự án nhỏ với C++

Bài 39.5: Bài tập thực hành dự án nhỏ với C++
Chào mừng các bạn đến với Bài 39.5 trong chuỗi bài học C++ của chúng ta!
Sau khi đã cùng nhau đi qua rất nhiều khái niệm cơ bản và nâng cao hơn một chút về ngôn ngữ C++, từ biến, kiểu dữ liệu, cấu trúc điều khiển, hàm, đến mảng, con trỏ (và có thể cả cấu trúc/lớp ở các bài trước đó), đã đến lúc chúng ta xắn tay áo lên và áp dụng tất cả những kiến thức đó vào việc xây dựng một thứ gì đó thực tế. Lý thuyết rất quan trọng, nhưng chỉ khi chúng ta thực hành, viết code, gặp lỗi, và sửa lỗi, kiến thức đó mới thực sự ngấm sâu và trở thành kỹ năng của mình.
Bài học này không đi sâu vào một khái niệm mới nào cả, mà tập trung vào việc tổng hợp và áp dụng. Chúng ta sẽ cùng nhau xây dựng một dự án nhỏ đầu tiên.
Tại sao lại cần dự án nhỏ?
Lý do rất đơn giản:
- Kết nối kiến thức: Các khái niệm C++ rời rạc sẽ được xâu chuỗi lại để giải quyết một bài toán cụ thể.
- Hiểu rõ hơn về luồng chương trình: Bạn sẽ thấy cách các phần khác nhau của chương trình (nhập liệu, xử lý, xuất kết quả) hoạt động cùng nhau.
- Rèn luyện kỹ năng gỡ lỗi: Khi viết code thực tế, lỗi là điều không thể tránh khỏi. Việc tìm và sửa lỗi là một kỹ năng cực kỳ quan trọng.
- Xây dựng sự tự tin: Hoàn thành một dự án nhỏ, dù đơn giản, mang lại cảm giác thành tựu và động lực để học tiếp những điều phức tạp hơn.
- Chuẩn bị cho các dự án lớn hơn: Các dự án phức tạp hơn đều được xây dựng từ những khối nhỏ. Bắt đầu với dự án nhỏ giúp bạn làm quen với quy trình.
Dự án đầu tiên: Máy tính đơn giản (Command-Line Calculator)
Để khởi động, chúng ta sẽ xây dựng một ứng dụng máy tính đơn giản chạy trên cửa sổ dòng lệnh (terminal/console). Dự án này đủ đơn giản để hoàn thành trong một buổi, nhưng đủ phức tạp để sử dụng các kiến thức sau:
- Nhập/Xuất dữ liệu (
cin
,cout
) - Biến và kiểu dữ liệu (số, ký tự)
- Cấu trúc điều khiển (
if-else if-else
hoặcswitch
, vòng lặpwhile
hoặcdo-while
) - Hàm (để tổ chức mã nguồn)
Chức năng chính của máy tính này sẽ là:
- Nhận vào hai số từ người dùng.
- Nhận vào phép toán (
+
,-
,*
,/
). - Thực hiện phép tính tương ứng.
- In kết quả ra màn hình.
- Có thể thực hiện nhiều phép tính liên tiếp cho đến khi người dùng muốn thoát.
- Xử lý trường hợp chia cho số 0.
Chúng ta hãy cùng nhau xây dựng từng bước nhé!
Bước 1: Khung chương trình cơ bản
Mọi chương trình C++ đều bắt đầu từ hàm main
. Chúng ta cần bao gồm thư viện iostream
để làm việc với nhập/xuất.
#include <iostream>
int main() {
return 0;
}
Giải thích:
#include <iostream>
: Dòng này yêu cầu trình biên dịch bao gồm nội dung của thư việniostream
, cung cấp các chức năng để đọc dữ liệu từ bàn phím (cin
) và ghi dữ liệu ra màn hình (cout
,cerr
).int main()
: Đây là hàm chính, điểm bắt đầu thực thi của mọi chương trình C++. Kiểu trả vềint
thường được dùng để báo hiệu trạng thái kết thúc chương trình (0 cho thành công, giá trị khác 0 cho lỗi).
Bước 2: Lấy đầu vào từ người dùng
Chúng ta cần hai số và một ký tự cho phép toán.
#include <iostream>
int main() {
double so1, so2;
char pt;
cout << "--- MAY TINH DON GIAN ---\n";
cout << "Nhap so thu nhat: ";
cin >> so1;
cout << "Nhap phep tinh (+, -, *, /): ";
cin >> pt;
cout << "Nhap so thu hai: ";
cin >> so2;
return 0;
}
Output:
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): +
Nhap so thu hai: 5
Giải thích:
- Chúng ta khai báo các biến
so1
,so2
kiểudouble
(để làm việc với số thập phân) vàpt
kiểuchar
(để lưu ký tự phép toán). cout << "..."
: Dùng để in thông báo ra màn hình, hướng dẫn người dùng nhập gì.\n
tạo xuống dòng mới.cin >> bien
: Dùng để đọc dữ liệu từ bàn phím và lưu vào biếnbien
.cin
sẽ tự động cố gắng chuyển đổi dữ liệu nhập vào sang kiểu dữ liệu của biến.
Bước 3: Thực hiện phép tính
Dựa vào ký tự pt
, chúng ta sẽ thực hiện phép tính tương ứng. Cấu trúc switch
rất phù hợp ở đây. Chúng ta cũng cần xử lý trường hợp chia cho 0 và phép toán không hợp lệ.
#include <iostream>
int main() {
double so1, so2, kq;
char pt;
bool hopLe = true;
cout << "--- MAY TINH DON GIAN ---\n";
cout << "Nhap so thu nhat: ";
cin >> so1;
cout << "Nhap phep tinh (+, -, *, /): ";
cin >> pt;
cout << "Nhap so thu hai: ";
cin >> so2;
switch (pt) {
case '+':
kq = so1 + so2;
break;
case '-':
kq = so1 - so2;
break;
case '*':
kq = so1 * so2;
break;
case '/':
if (so2 != 0) {
kq = so1 / so2;
} else {
cerr << "Loi: Khong the chia cho khong!\n";
hopLe = false;
}
break;
default:
cerr << "Loi: Phep tinh khong hop le!\n";
hopLe = false;
break;
}
if (hopLe) {
cout << "Ket qua: " << kq << endl;
}
return 0;
}
Output:
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): +
Nhap so thu hai: 5
Ket qua: 15
Output (chia cho 0):
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): /
Nhap so thu hai: 0
Loi: Khong the chia cho khong!
Output (phép toán không hợp lệ):
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): %
Nhap so thu hai: 3
Loi: Phep tinh khong hop le!
Giải thích:
- Biến
kq
kiểudouble
được khai báo để chứa kết quả phép tính. - Biến
hopLe
kiểubool
được sử dụng như một cờ để theo dõi liệu phép tính có thành công hay gặp lỗi (chia 0, phép toán sai). Ban đầu giả định là hợp lệ (true
). - Cấu trúc
switch (pt)
kiểm tra giá trị của biếnpt
. - Mỗi
case
tương ứng với một ký hiệu phép toán. Mã bên trongcase
thực hiện phép tính tương ứng. - Lệnh
break;
sau mỗicase
là rất quan trọng để thoát khỏi khốiswitch
sau khi case đó được xử lý. Nếu không cóbreak
, chương trình sẽ tiếp tục thực hiện mã của các case tiếp theo (gọi là "fall-through"), điều này thường không mong muốn. - Trong
case '/'
, chúng ta kiểm traif (so2 != 0)
để tránh lỗi chia cho 0. Nếuso2
bằng 0, chúng ta in thông báo lỗi racerr
(luồng lỗi chuẩn) và đặthopLe = false;
. - Khối
default:
xử lý mọi ký tựpt
không khớp với bất kỳcase
nào đã định nghĩa. Nó cũng in lỗi và đặthopLe = false;
. - Câu lệnh
if (hopLe)
đảm bảo rằng dòng in kết quả chỉ chạy khi mọi thứ diễn ra suôn sẻ. endl
không chỉ xuống dòng mà còn đẩy bộ đệm xuất (flush the output buffer), đảm bảo dữ liệu hiển thị ngay lập tức.
Bước 4: Thực hiện nhiều phép tính (vòng lặp)
Chúng ta muốn máy tính này có thể thực hiện nhiều phép tính liên tiếp mà không cần chạy lại chương trình. Vòng lặp do-while
hoặc while
là lựa chọn tốt. do-while
có vẻ hợp lý vì chúng ta muốn chạy ít nhất một lần trước khi hỏi người dùng có muốn tiếp tục hay không.
Chúng ta sẽ bao bọc phần lấy đầu vào, tính toán và in kết quả vào một vòng lặp do-while
và thêm câu hỏi cho người dùng ở cuối mỗi lần lặp.
#include <iostream>
#include <limits>
int main() {
char tiepTuc;
do {
double so1, so2, kq;
char pt;
bool hopLe = true;
cout << "\n--- MAY TINH DON GIAN ---\n";
cout << "Nhap so thu nhat: ";
cin >> so1;
if (cin.fail()) {
cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
cout << "Nhap phep tinh (+, -, *, /): ";
cin >> pt;
cout << "Nhap so thu hai: ";
cin >> so2;
if (cin.fail()) {
cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
switch (pt) {
case '+':
kq = so1 + so2;
break;
case '-':
kq = so1 - so2;
break;
case '*':
kq = so1 * so2;
break;
case '/':
if (so2 != 0) {
kq = so1 / so2;
} else {
cerr << "Loi: Khong the chia cho khong!\n";
hopLe = false;
}
break;
default:
cerr << "Loi: Phep tinh khong hop le!\n";
hopLe = false;
break;
}
if (hopLe) {
cout << "Ket qua: " << kq << endl;
}
cout << "Ban co muon thuc hien phep tinh khac khong? (y/n): ";
cin >> tiepTuc;
cin.ignore(numeric_limits<streamsize>::max(), '\n');
} while (tiepTuc == 'y' || tiepTuc == 'Y');
cout << "Tam biet!\n";
return 0;
}
Output:
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): +
Nhap so thu hai: 5
Ket qua: 15
Ban co muon thuc hien phep tinh khac khong? (y/n): y
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): /
Nhap so thu hai: 0
Loi: Khong thể chia cho khong!
Ban co muon thuc hien phep tinh khac khong? (y/n): n
Tam biet!
Giải thích:
- Chúng ta thêm một vòng lặp
do { ... } while (...)
. Toàn bộ logic lấy đầu vào, tính toán, in kết quả nằm trong phầndo { ... }
. - Sau khi xử lý một phép tính, chương trình hỏi người dùng nhập 'y' (có) hoặc 'n' (không) để tiếp tục. Lựa chọn này được lưu vào biến
tiepTuc
. - Điều kiện của vòng lặp
while (tiepTuc == 'y' || tiepTuc == 'Y')
kiểm tra xem người dùng có nhập 'y' hoặc 'Y' không. Nếu có, vòng lặp tiếp tục. Nếu nhập ký tự khác, vòng lặp kết thúc. cin.fail()
,cin.clear()
,cin.ignore(...)
: Đây là cách cơ bản để xử lý lỗi khi người dùng nhập sai kiểu dữ liệu (ví dụ, nhập chữ khi chương trình chờ số).cin.fail()
: Trả vềtrue
nếu thao tác đọc trước đó thất bại (ví dụ: cố đọc số nhưng nhận chữ).cin.clear()
: Xóa cờ lỗi củacin
để có thể tiếp tục đọc.cin.ignore(numeric_limits<streamsize>::max(), '\n')
: Bỏ qua (loại bỏ) các ký tự còn lại trong bộ đệm nhập liệu củacin
cho đến cuối dòng (\n
), tránh ảnh hưởng đến lần đọc tiếp theo trong vòng lặp. Điều này đặc biệt quan trọng sau khi đọc mộtchar
hoặc một số, vì ký tự xuống dòng vẫn còn lại trong bộ đệm.
continue;
: Lệnh này bên trong vòng lặpdo-while
sẽ bỏ qua phần còn lại của lần lặp hiện tại và nhảy đến phần kiểm tra điều kiện củawhile
(trong trường hợp lỗi nhập liệu, chúng ta muốn hỏi lại người dùng ngay).
Bước 5: Tổ chức mã nguồn với Hàm
Mặc dù chương trình hiện tại không quá dài, việc đưa logic tính toán vào một hàm riêng sẽ giúp mã nguồn rõ ràng hơn, dễ đọc và dễ bảo trì hơn (nguyên tắc chia để trị!).
Chúng ta sẽ tạo một hàm có tên tinh
nhận vào hai số và phép toán, và trả về kết quả. Hàm này cũng cần báo hiệu nếu có lỗi (như chia cho 0). Một cách là trả về một giá trị đặc biệt cho lỗi, hoặc tốt hơn là dùng tham số truyền qua tham chiếu để trả về kết quả và dùng giá trị trả về của hàm (bool
) để báo hiệu thành công hay thất bại.
#include <iostream>
#include <limits>
bool tinh(double s1, double s2, char p, double& kq) {
switch (p) {
case '+':
kq = s1 + s2;
return true;
case '-':
kq = s1 - s2;
return true;
case '*':
kq = s1 * s2;
return true;
case '/':
if (s2 != 0) {
kq = s1 / s2;
return true;
} else {
cerr << "Loi: Khong the chia cho khong!\n";
return false;
}
default:
cerr << "Loi: Phep tinh khong hop le!\n";
return false;
}
}
int main() {
char tiepTuc;
do {
double so1, so2, kqTinh;
char pt;
cout << "\n--- MAY TINH DON GIAN ---\n";
cout << "Nhap so thu nhat: ";
cin >> so1;
if (cin.fail()) {
cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
cout << "Nhap phep tinh (+, -, *, /): ";
cin >> pt;
cout << "Nhap so thu hai: ";
cin >> so2;
if (cin.fail()) {
cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
if (tinh(so1, so2, pt, kqTinh)) {
cout << "Ket qua: " << kqTinh << endl;
}
cout << "Ban co muon thuc hien phep tinh khac khong? (y/n): ";
cin >> tiepTuc;
cin.ignore(numeric_limits<streamsize>::max(), '\n');
} while (tiepTuc == 'y' || tiepTuc == 'Y');
cout << "Tam biet!\n";
return 0;
}
Output:
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): +
Nhap so thu hai: 5
Ket qua: 15
Ban co muon thuc hien phep tinh khac khong? (y/n): y
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): /
Nhap so thu hai: 0
Loi: Khong the chia cho khong!
Ban co muon thuc hien phep tinh khac khong? (y/n): n
Tam biet!
Giải thích:
- Chúng ta định nghĩa hàm
bool tinh(double s1, double s2, char p, double& kq)
.- Nó nhận hai số
s1
,s2
và phép toánp
theo giá trị (pass by value) - tức là bản sao của giá trị được truyền vào. - Nó nhận
kq
theo tham chiếu (double& kq
). Điều này có nghĩa là hàm làm việc trực tiếp trên biếnkqTinh
trong hàmmain
(hoặc bất kỳ biến nào được truyền vào ở vị trí tham số thứ tư), cho phép hàm thay đổi giá trị của biến đó. - Hàm trả về
bool
:true
nếu phép tính hợp lệ và thành công,false
nếu gặp lỗi (chia 0 hoặc phép toán sai).
- Nó nhận hai số
- Bên trong
main
, sau khi lấy đầu vào, chúng ta gọi hàmtinh
:if (tinh(so1, so2, pt, kqTinh))
. - Lệnh
if
kiểm tra giá trị trả về củatinh
. Nếu làtrue
, chúng ta inkqTinh
(biến này đã được hàmtinh
gán giá trị). Nếu làfalse
, chúng ta không làm gì thêm trongmain
vì hàmtinh
đã in thông báo lỗi rồi. - Việc sử dụng hàm
tinh
làm cho hàmmain
trở nên gọn gàng và dễ đọc hơn nhiều.main
giờ đây chỉ tập trung vào luồng chính: lặp, lấy đầu vào, gọi hàm xử lý, hỏi tiếp tục.
Toàn bộ mã nguồn dự án "Máy tính đơn giản"
Đây là toàn bộ mã nguồn của ứng dụng máy tính đơn giản sau khi đã được tổ chức lại với hàm và vòng lặp:
#include <iostream>
#include <limits>
bool tinh(double s1, double s2, char p, double& kq) {
switch (p) {
case '+':
kq = s1 + s2;
return true;
case '-':
kq = s1 - s2;
return true;
case '*':
kq = s1 * s2;
return true;
case '/':
if (s2 != 0) {
kq = s1 / s2;
return true;
} else {
cerr << "Loi: Khong the chia cho khong!\n";
return false;
}
default:
cerr << "Loi: Phep tinh khong hop le!\n";
return false;
}
}
int main() {
char tiepTuc;
do {
double so1, so2, kqTinh;
char pt;
cout << "\n--- MAY TINH DON GIAN ---\n";
cout << "Nhap so thu nhat: ";
cin >> so1;
if (cin.fail()) {
cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
cout << "Nhap phep tinh (+, -, *, /): ";
cin >> pt;
cout << "Nhap so thu hai: ";
cin >> so2;
if (cin.fail()) {
cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}
if (tinh(so1, so2, pt, kqTinh)) {
cout << "Ket qua: " << kqTinh << endl;
}
cout << "Ban co muon thuc hien phep tinh khac khong? (y/n): ";
cin >> tiepTuc;
cin.ignore(numeric_limits<streamsize>::max(), '\n');
} while (tiepTuc == 'y' || tiepTuc == 'Y');
cout << "Tam biet!\n";
return 0;
}
Output:
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): +
Nhap so thu hai: 5
Ket qua: 15
Ban co muon thuc hien phep tinh khac khong? (y/n): y
--- MAY TINH DON GIAN ---
Nhap so thu nhat: 10
Nhap phep tinh (+, -, *, /): /
Nhap so thu hai: 0
Loi: Khong the chia cho khong!
Ban co muon thuc hien phep tinh khac khong? (y/n): n
Tam biet!
Các bước để chạy chương trình này:
- Lưu toàn bộ mã nguồn trên vào một tệp có đuôi
.cpp
(ví dụ:calculator.cpp
). - Mở cửa sổ dòng lệnh (terminal hoặc Command Prompt).
- Sử dụng trình biên dịch C++ (ví dụ: g++) để biên dịch tệp:
g++ calculator.cpp -o calculator
- Chạy tệp thực thi vừa tạo:
- Trên Linux/macOS:
./calculator
- Trên Windows:
calculator.exe
(hoặc chỉcalculator
)
- Trên Linux/macOS:
Bây giờ bạn đã có một ứng dụng máy tính đơn giản của riêng mình được viết bằng C++!
Vượt ra ngoài giới hạn cơ bản
Dự án máy tính đơn giản này chỉ là khởi đầu. Bạn có thể mở rộng nó theo nhiều cách:
- Thêm các phép toán: Căn bậc hai (
sqrt
, cần#include <cmath>
), lũy thừa, phần trăm, v.v. - Hỗ trợ số phức: Nếu bạn đã học về cấu trúc hoặc lớp, bạn có thể tạo một kiểu dữ liệu cho số phức.
- Lịch sử phép tính: Lưu lại các phép tính đã thực hiện trong một danh sách (ví dụ:
vector
nếu bạn đã học). - Giao diện người dùng (GUI): Thay vì dòng lệnh, xây dựng một giao diện đồ họa đơn giản (điều này sẽ cần thư viện ngoài và là một chủ đề nâng cao hơn).
- Đọc từ tệp/Ghi ra tệp: Lưu và tải lịch sử phép tính từ một tệp văn bản.
- Xử lý biểu thức phức tạp: Thay vì chỉ hai số và một phép toán, cho phép người dùng nhập cả biểu thức như
(2 + 3) * 5
. Điều này đòi hỏi kiến thức về phân tích cú pháp (parsing) và cấu trúc dữ liệu (ví dụ: stack).
Comments