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> // Bao gồm thư viện cho nhập/xuất

int main() {
    // Nơi code logic của máy tính sẽ được viết

    return 0; // Trả về 0 báo hiệu chương trình kết thúc thành công
}

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 num1, num2; // Sử dụng double để có thể nhập số thực
    char op;           // Sử dụng char để lưu ký hiệu phép toán

    cout << "--- MAY TINH DON GIAN ---\n";

    cout << "Nhap so thu nhat: ";
    cin >> num1; // Đọc số thứ nhất từ bàn phím

    cout << "Nhap phep tinh (+, -, *, /): ";
    cin >> op;   // Đọc ký hiệu phép toán

    cout << "Nhap so thu hai: ";
    cin >> num2; // Đọc số thứ hai từ bàn phím

    // ... logic tính toán sẽ ở đây ...

    return 0;
}

Giải thích:

  • Chúng ta khai báo các biến num1, num2 kiểu double (để làm việc với số thập phân) và op 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ự op, 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>
#include <limits> // Cần cho thao tác bỏ qua dữ liệu thừa sau cin

int main() {
    double num1, num2;
    char op;
    double result; // Biến để lưu kết quả

    cout << "--- MAY TINH DON GIAN ---\n";

    cout << "Nhap so thu nhat: ";
    cin >> num1;

    cout << "Nhap phep tinh (+, -, *, /): ";
    cin >> op;

    cout << "Nhap so thu hai: ";
    cin >> num2;

    // Biến cờ để kiểm tra xem phép tính có hợp lệ không
    bool valid_calculation = true; 

    switch (op) {
        case '+':
            result = num1 + num2;
            break; // Thoát khỏi switch sau khi thực hiện xong case
        case '-':
            result = num1 - num2;
            break;
        case '*':
            result = num1 * num2;
            break;
        case '/':
            // Kiểm tra chia cho 0
            if (num2 != 0) {
                result = num1 / num2;
            } else {
                cerr << "Loi: Khong the chia cho khong!\n";
                valid_calculation = false; // Đánh dấu là phép tính không hợp lệ
            }
            break;
        default:
            // Trường hợp phép toán không hợp lệ
            cerr << "Loi: Phep tinh khong hop le!\n";
            valid_calculation = false; // Đánh dấu là phép tính không hợp lệ
            break;
    }

    // Chỉ in kết quả nếu phép tính hợp lệ
    if (valid_calculation) {
        cout << "Ket qua: " << result << endl;
    }

    // Lưu ý: Đến đây chương trình sẽ kết thúc sau một phép tính

    return 0;
}

Giải thích:

  • Biến result kiểu double được khai báo để chứa kết quả phép tính.
  • Biến valid_calculation 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 (op) kiểm tra giá trị của biến op.
  • 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 (num2 != 0) để tránh lỗi chia cho 0. Nếu num2 bằng 0, chúng ta in thông báo lỗi ra cerr (luồng lỗi chuẩn) và đặt valid_calculation = false;.
  • Khối default: xử lý mọi ký tự op không khớp với bất kỳ case nào đã định nghĩa. Nó cũng in lỗi và đặt valid_calculation = false;.
  • Câu lệnh if (valid_calculation) đả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> // Cần cho thao tác bỏ qua dữ liệu thừa sau cin

int main() {
    char continue_choice; // Biến để lưu lựa chọn tiếp tục của người dùng

    // Vòng lặp do-while để thực hiện ít nhất một lần và lặp lại nếu người dùng chọn
    do {
        double num1, num2, result;
        char op;

        cout << "\n--- MAY TINH DON GIAN ---\n"; // Thêm xuống dòng cho dễ nhìn

        cout << "Nhap so thu nhat: ";
        cin >> num1;

        // Kiểm tra lỗi nhập liệu cho số thứ nhất
        if (cin.fail()) {
            cerr << "Loi nhap lieu: Vui long nhap mot so hop le.\n";
            // Xóa trạng thái lỗi của cin
            cin.clear(); 
            // Bỏ qua phần còn lại của dòng nhập liệu
            cin.ignore(numeric_limits<streamsize>::max(), '\n'); 
            // Bỏ qua lần lặp này và hỏi lại
            continue; 
        }


        cout << "Nhap phep tinh (+, -, *, /): ";
        cin >> op;

        cout << "Nhap so thu hai: ";
        cin >> num2;

        // Kiểm tra lỗi nhập liệu cho số thứ hai
        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; // Bỏ qua lần lặp này
        }

        bool valid_calculation = true;

        switch (op) {
            case '+':
                result = num1 + num2;
                break;
            case '-':
                result = num1 - num2;
                break;
            case '*':
                result = num1 * num2;
                break;
            case '/':
                if (num2 != 0) {
                    result = num1 / num2;
                } else {
                    cerr << "Loi: Khong the chia cho khong!\n";
                    valid_calculation = false;
                }
                break;
            default:
                cerr << "Loi: Phep tinh khong hop le!\n";
                valid_calculation = false;
                break;
        }

        if (valid_calculation) {
            cout << "Ket qua: " << result << endl;
        }

        // Hỏi người dùng có muốn tiếp tục
        cout << "Ban co muon thuc hien phep tinh khac khong? (y/n): ";
        // Đọc lựa chọn và bỏ qua các ký tự thừa sau 'y' hoặc 'n'
        cin >> continue_choice;
        cin.ignore(numeric_limits<streamsize>::max(), '\n'); // Rất quan trọng sau khi đọc char/string

    } while (continue_choice == 'y' || continue_choice == 'Y'); // Tiếp tục nếu người dùng nhập 'y' hoặc 'Y'

    cout << "Tam biet!\n"; // Lời chào khi thoát

    return 0;
}

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 continue_choice.
  • Điều kiện của vòng lặp while (continue_choice == 'y' || continue_choice == '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 calculate 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> // Cần cho thao tác bỏ qua dữ liệu thừa sau cin

// Khai báo nguyên mẫu hàm (function prototype)
// bool calculate(double num1, double num2, char op, double& result); 
// Hoặc định nghĩa hàm đầy đủ trước hàm main

// Hàm thực hiện phép tính
// Trả về true nếu tính toán thành công, false nếu có lỗi (ví dụ: chia cho 0, phép toán sai)
// Kết quả được lưu vào tham số 'result' (truyền qua tham chiếu)
bool calculate(double num1, double num2, char op, double& result) {
    switch (op) {
        case '+':
            result = num1 + num2;
            return true; // Thành công
        case '-':
            result = num1 - num2;
            return true; // Thành công
        case '*':
            result = num1 * num2;
            return true; // Thành công
        case '/':
            if (num2 != 0) {
                result = num1 / num2;
                return true; // Thành công
            } else {
                // Lỗi chia cho 0
                cerr << "Loi: Khong the chia cho khong!\n";
                return false; // Thất bại
            }
        default:
            // Lỗi phép toán không hợp lệ
            cerr << "Loi: Phep tinh khong hop le!\n";
            return false; // Thất bại
    }
}

int main() {
    char continue_choice;

    do {
        double num1, num2, calculation_result; // Đổi tên biến kết quả trong main cho rõ ràng
        char op;

        cout << "\n--- MAY TINH DON GIAN ---\n";

        cout << "Nhap so thu nhat: ";
        cin >> num1;

        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 >> op;

        cout << "Nhap so thu hai: ";
        cin >> num2;

        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;
        }

        // Gọi hàm calculate để thực hiện phép tính
        if (calculate(num1, num2, op, calculation_result)) {
            // Nếu hàm calculate trả về true (thành công), in kết quả
            cout << "Ket qua: " << calculation_result << endl;
        }
        // Nếu hàm calculate trả về false, nó đã tự in thông báo lỗi bên trong rồi.

        // Hỏi người dùng có muốn tiếp tục
        cout << "Ban co muon thuc hien phep tinh khac khong? (y/n): ";
        cin >> continue_choice;
        cin.ignore(numeric_limits<streamsize>::max(), '\n');

    } while (continue_choice == 'y' || continue_choice == 'Y');

    cout << "Tam biet!\n";

    return 0;
}

Giải thích:

  • Chúng ta định nghĩa hàm bool calculate(double num1, double num2, char op, double& result).
    • Nó nhận hai số num1, num2 và phép toán op theo giá trị (pass by value) - tức là bản sao của giá trị được truyền vào.
    • Nó nhận result theo tham chiếu (double& result). Điều này có nghĩa là hàm làm việc trực tiếp trên biến calculation_result 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 calculate: if (calculate(num1, num2, op, calculation_result)).
  • Lệnh if kiểm tra giá trị trả về của calculate. Nếu là true, chúng ta in calculation_result (biến này đã được hàm calculate gán giá trị). Nếu là false, chúng ta không làm gì thêm trong main vì hàm calculate đã in thông báo lỗi rồi.
  • Việc sử dụng hàm calculate 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> // Cần cho thao tác bỏ qua dữ liệu thừa sau cin
#include <string> // Có thể hữu ích cho các dự án sau, nhưng không bắt buộc ở đây

// Hàm thực hiện phép tính
// Trả về true nếu tính toán thành công, false nếu có lỗi (ví dụ: chia cho 0, phép toán sai)
// Kết quả được lưu vào tham số 'result' (truyền qua tham chiếu)
bool calculate(double num1, double num2, char op, double& result) {
    switch (op) {
        case '+':
            result = num1 + num2;
            return true; // Thành công
        case '-':
            result = num1 - num2;
            return true; // Thành công
        case '*':
            result = num1 * num2;
            return true; // Thành công
        case '/':
            if (num2 != 0) {
                result = num1 / num2;
                return true; // Thành công
            } else {
                // Lỗi chia cho 0
                cerr << "Loi: Khong the chia cho khong!\n";
                return false; // Thất bại
            }
        default:
            // Lỗi phép toán không hợp lệ
            cerr << "Loi: Phep tinh khong hop le!\n";
            return false; // Thất bại
    }
}

int main() {
    char continue_choice;

    // Vòng lặp do-while để thực hiện ít nhất một lần và lặp lại nếu người dùng chọn
    do {
        double num1, num2, calculation_result;
        char op;

        cout << "\n--- MAY TINH DON GIAN ---\n"; 

        cout << "Nhap so thu nhat: ";
        cin >> num1;

        // Kiểm tra lỗi nhập liệu cho số thứ nhất
        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 >> op;

        // Có thể thêm kiểm tra lỗi nhập liệu cho ký tự phép toán ở đây
        // Tuy nhiên, switch default case cũng xử lý được trường hợp nhập ký tự không hợp lệ

        cout << "Nhap so thu hai: ";
        cin >> num2;

        // Kiểm tra lỗi nhập liệu cho số thứ hai
        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; // Bỏ qua lần lặp này
        }

        // Gọi hàm calculate để thực hiện phép tính
        if (calculate(num1, num2, op, calculation_result)) {
            // Nếu hàm calculate trả về true (thành công), in kết quả
            cout << "Ket qua: " << calculation_result << endl;
        }
        // Nếu hàm calculate trả về false, nó đã tự in thông báo lỗi bên trong rồi.

        // Hỏi người dùng có muốn tiếp tục
        cout << "Ban co muon thuc hien phep tinh khac khong? (y/n): ";
        cin >> continue_choice;
        // Quan trọng: Bỏ qua các ký tự còn lại trong bộ đệm nhập liệu sau khi đọc 'y'/'n'
        // Nếu không có dòng này, ký tự xuống dòng ('\n') còn lại sẽ khiến 
        // cin >> num1 ở lần lặp tiếp theo gặp lỗi ngay lập tức nếu nó không được xử lý.
        cin.ignore(numeric_limits<streamsize>::max(), '\n'); 

    } while (continue_choice == 'y' || continue_choice == 'Y');

    cout << "Tam biet!\n";

    return 0;
}
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.