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á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á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:

  1. 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ể.
  2. 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.
  3. 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.
  4. 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.
  5. 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ặc switch, vòng lặp while hoặc do-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à:

  1. Nhận vào hai số từ người dùng.
  2. Nhận vào phép toán (+, -, *, /).
  3. Thực hiện phép tính tương ứng.
  4. In kết quả ra màn hình.
  5. 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.
  6. 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ện iostream, 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ểu double (để làm việc với số thập phân) và pt kiểu char (để 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ến bien. 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ểu double được khai báo để chứa kết quả phép tính.
  • Biến hopLe kiểu bool đượ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ến pt.
  • Mỗi case tương ứng với một ký hiệu phép toán. Mã bên trong case thực hiện phép tính tương ứng.
  • Lệnh break; sau mỗi caserất quan trọng để thoát khỏi khối switch 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 tra if (so2 != 0) để tránh lỗi chia cho 0. Nếu so2 bằng 0, chúng ta in thông báo lỗi ra cerr (luồng lỗi chuẩn) và đặt hopLe = 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à đặt hopLe = 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ần do { ... }.
  • 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ủa cin để 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ủa cin 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ột char 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ặp do-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ủa while (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ả 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án p 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ến kqTinh trong hàm main (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).
  • Bên trong main, sau khi lấy đầu vào, chúng ta gọi hàm tinh: if (tinh(so1, so2, pt, kqTinh)).
  • Lệnh if kiểm tra giá trị trả về của tinh. Nếu là true, chúng ta in kqTinh (biến này đã được hàm tinh gán giá trị). Nếu là false, chúng ta không làm gì thêm trong main vì hàm tinh đã in thông báo lỗi rồi.
  • Việc sử dụng hàm tinh làm cho hàm main 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:
  1. Lưu toàn bộ mã nguồn trên vào một tệp có đuôi .cpp (ví dụ: calculator.cpp).
  2. Mở cửa sổ dòng lệnh (terminal hoặc Command Prompt).
  3. Sử dụng trình biên dịch C++ (ví dụ: g++) để biên dịch tệp: g++ calculator.cpp -o calculator
  4. 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)

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

There are no comments at the moment.