Bài 2.5: Bài tập thực hành rẽ nhánh nâng cao trong C++

Chào mừng trở lại với series C++! Ở các bài trước, chúng ta đã làm quen với những công cụ rẽ nhánh cơ bản như ifif/else để đưa ra quyết định đơn giản dựa trên một điều kiện. Tuyệt vời! Nhưng trong thế giới thực của lập trình, các tình huống thường phức tạp hơn nhiều.

Điều gì xảy ra khi chúng ta cần kiểm tra nhiều điều kiện cùng lúc? Hoặc khi một quyết định phụ thuộc vào kết quả của một quyết định khác? Đó chính là lúc các kỹ thuật rẽ nhánh nâng cao phát huy tác dụng. Bài viết này sẽ đi sâu vào các phương pháp xử lý rẽ nhánh phức tạp hơn, giúp code của bạn mạnh mẽ và linh hoạt hơn.

Chúng ta sẽ cùng nhau thực hành với các khái niệm như rẽ nhánh lồng nhau, điều kiện phức hợp sử dụng các toán tử logic, và tìm hiểu sâu hơn về câu lệnh switch.

1. Rẽ nhánh Lồng nhau (Nested Branching)

Khái niệm rẽ nhánh lồng nhau khá đơn giản: đó là việc đặt một câu lệnh rẽ nhánh (if, if/else, hoặc switch) bên trong khối lệnh của một câu lệnh rẽ nhánh khác. Điều này cho phép bạn xử lý các quyết định phụ thuộc lẫn nhau hoặc có thứ bậc.

Hãy tưởng tượng bạn cần kiểm tra xem một người có đủ điều kiện tham gia một sự kiện đặc biệt hay không. Điều kiện đầu tiên là họ phải đủ tuổi, và chỉ khi họ đủ tuổi, bạn mới kiểm tra điều kiện thứ hai, ví dụ: họ có phải là cư dân của một khu vực cụ thể hay không.

Ví dụ 1.1: Kiểm tra tuổi và địa điểm

Chương trình này kiểm tra xem một người có đủ 18 tuổi hay không. Nếu đủ 18 tuổi, nó tiếp tục kiểm tra xem họ có sống ở "Hà Nội" hay không.

#include <iostream>
#include <string>

int main() {
    int tuoi = 20;
    string thanhPho = "Hanoi"; // Thử thay đổi thành "HCM" để xem kết quả

    cout << "Dang kiem tra dieu kien..." << endl;

    // Rẽ nhánh ngoài: Kiểm tra tuổi
    if (tuoi >= 18) {
        cout << "-> Da du tuoi." << endl;

        // Rẽ nhánh lồng nhau bên trong: Kiểm tra thành phố (chi khi du tuoi)
        if (thanhPho == "Hanoi") {
            cout << "-> Va ban song o Ha Noi. -> **DU DIEU KIEN!**" << endl;
        } else {
            cout << "-> Nhung ban KHONG song o Ha Noi. -> Chua du dieu kien hoan toan." << endl;
        }
    } else {
        cout << "-> Chua du tuoi. -> Chua du dieu kien." << endl;
    }

    return 0;
}

Giải thích:

  • Câu lệnh if (tuoi >= 18) là rẽ nhánh ngoài. Khối lệnh bên trong nó chỉ được thực thi khi tuổi lớn hơn hoặc bằng 18.
  • Bên trong khối if đầu tiên, chúng ta có một câu lệnh if/else khác (if (thanhPho == "Hanoi")). Đây chính là rẽ nhánh lồng nhau.
  • Câu lệnh else cho if ngoài sẽ xử lý trường hợp người đó không đủ tuổi, và các kiểm tra lồng nhau bên trong sẽ không bao giờ được thực thi.

Rẽ nhánh lồng nhau rất hữu ích, nhưng hãy cẩn thận! Việc lồng quá sâu có thể khiến code trở nên khó đọc và khó quản lý. Đôi khi, việc kết hợp các điều kiện bằng toán tử logic là một lựa chọn tốt hơn.

2. Điều kiện phức hợp với Toán tử Logic

Thay vì lồng nhiều lớp if, bạn có thể kết hợp nhiều điều kiện đơn giản thành một điều kiện phức hợp bằng cách sử dụng các toán tử logic:

  • && (AND): Điều kiện phức hợp đúng khi tất cả các điều kiện con đều đúng.
  • || (OR): Điều kiện phức hợp đúng khi ít nhất một trong các điều kiện con đúng.
  • ! (NOT): Đảo ngược giá trị boolean của một điều kiện (đúng thành sai, sai thành đúng).

Việc sử dụng toán tử logic giúp làm phẳng cấu trúc rẽ nhánh, thường cải thiện khả năng đọc code, đặc biệt là khi các điều kiện không có mối quan hệ thứ bậc rõ ràng mà chỉ cần cùng nhau đáp ứng một tiêu chí nào đó.

Ví dụ 2.1: Sử dụng && (AND)

Kiểm tra xem một sinh viên có đủ điều kiện nhận học bổng hay không, dựa trên cả hai điều kiện: điểm trung bình (GPA) cao hơn 3.0 đang tích cực theo học.

#include <iostream>

int main() {
    double gpa = 3.8;
    bool dangHoc = true; // Đang theo học

    // Điều kiện phức hợp: GPA > 3.0 AND đang học
    if (gpa > 3.0 && dangHoc) {
        cout << "Ban du dieu kien nhan hoc bong." << endl;
    } else {
        cout << "Ban khong du dieu kien nhan hoc bong." << endl;
    }

    // Thử thay đổi gpa hoặc dangHoc để xem kết quả
    gpa = 2.9;
    dangHoc = true;
    if (gpa > 3.0 && dangHoc) {
        cout << "[Lan 2] Ban du dieu kien nhan hoc bong." << endl;
    } else {
        cout << "[Lan 2] Ban khong du dieu kien nhan hoc bong." << endl; // Kết quả sẽ là dòng này
    }


    return 0;
}

Giải thích: Câu lệnh if (gpa > 3.0 && dangHoc) chỉ đúng khi cả gpa > 3.0true dangHoctrue. Nếu một trong hai hoặc cả hai đều sai, điều kiện phức hợp sẽ sai.

Ví dụ 2.2: Sử dụng || (OR)

Kiểm tra xem một người có đủ điều kiện nhận vé giảm giá hay không, nếu họ hoặc là người cao tuổi (trên 65 tuổi) hoặc là trẻ em (dưới 12 tuổi).

#include <iostream>

int main() {
    int tuoi = 8; // Thử thay đổi thành 70 hoặc 30

    // Điều kiện phức hợp: Tuổi >= 65 OR Tuổi < 12
    if (tuoi >= 65 || tuoi < 12) {
        cout << "Ban du dieu kien mua ve giam gia." << endl;
    } else {
        cout << "Ban khong du dieu kien mua ve giam gia." << endl;
    }

    return 0;
}

Giải thích: Câu lệnh if (tuoi >= 65 || tuoi < 12) đúng nếu một trong hai điều kiện (tuoi >= 65 hoặc tuoi < 12) là true, hoặc cả hai đều true (dù trong ví dụ này không thể xảy ra cả hai cùng lúc). Chỉ khi cả hai điều kiện con đều sai, điều kiện phức hợp mới sai.

Ví dụ 2.3: Sử dụng ! (NOT)

Kiểm tra xem một người dùng chưa đăng nhập hay không để hiển thị thông báo yêu cầu đăng nhập.

#include <iostream>

int main() {
    bool daDangNhap = false; // Thử thay đổi thành true

    // Điều kiện: chuaDangNhap (nghia la KHONG phai daDangNhap)
    if (!daDangNhap) {
        cout << "Vui long dang nhap de tiep tuc." << endl;
    } else {
        cout << "Chao mung ban tro lai!" << endl;
    }

    return 0;
}

Giải thích: Toán tử ! đảo ngược giá trị boolean. !daDangNhap sẽ là true nếu daDangNhapfalse, và ngược lại.

Bạn có thể kết hợp các toán tử logic này để tạo ra các điều kiện phức tạp hơn nữa, ví dụ: (tuoi >= 18 && gioiTinh == "Nu") || (tuoi >= 20 && gioiTinh == "Nam"). Khi kết hợp nhiều toán tử, việc sử dụng dấu ngoặc đơn () để nhóm các điều kiện con là rất quan trọng để đảm bảo thứ tự ưu tiên và làm cho code dễ hiểu hơn.

3. Câu lệnh switch Nâng cao

Chúng ta đã biết switch rất hữu ích khi bạn cần so sánh một biến với nhiều giá trị hằng khác nhau. Tuy nhiên, có một vài điểm "nâng cao" hoặc cần lưu ý khi sử dụng switch:

  • Fall-through: Mặc định, sau khi một case được thực thi, luồng chương trình sẽ tiếp tục chạy xuống các case tiếp theo cho đến khi gặp break; hoặc kết thúc switch. Đây gọi là "fall-through". Thông thường, bạn sẽ muốn dùng break; ở cuối mỗi case để ngăn chặn điều này, chỉ thực thi khối lệnh tương ứng với case khớp đầu tiên. Tuy nhiên, đôi khi fall-through lại hữu ích (dù ít gặp).
  • default: Khối default sẽ được thực thi nếu không có case nào khớp với giá trị của biến. Việc sử dụng default rất quan trọng để xử lý các trường hợp không mong muốn hoặc không được định nghĩa rõ ràng.

Ví dụ 3.1: Sử dụng switch với breakdefault (Cách dùng phổ biến)

Chương trình kiểm tra số ngày trong một tháng nhất định (bỏ qua năm nhuận cho đơn giản).

#include <iostream>

int main() {
    int month = 2; // Thử thay đổi số tháng

    cout << "Thang " << month << " co ";

    switch (month) {
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
            cout << "31 ngay.";
            break; // Ngừng tại đây sau khi in
        case 4:
        case 6:
        case 9:
        case 11:
            cout << "30 ngay.";
            break; // Ngừng tại đây
        case 2:
            cout << "28 hoac 29 ngay (tuy nam nhuan).";
            break; // Ngừng tại đây
        default: // Nếu không khớp với bất kỳ case nào trên
            cout << "so thang khong hop le.";
    }
    cout << endl; // In xuống dòng cuối cùng

    return 0;
}

Giải thích:

  • Chúng ta gom các case có cùng kết quả (31 ngày, 30 ngày) lại với nhau. Nhờ có fall-through, nếu month là 1, nó khớp case 1:, nhưng không có break, nên nó tiếp tục chạy xuống case 3:, rồi case 5:,... cho đến khi gặp break; sau case 12:. Đây là một trong số ít trường hợp fall-through được sử dụng một cách có chủ đích.
  • Câu lệnh break; rất quan trọng để thoát khỏi switch sau khi tìm thấy case phù hợp và thực thi xong khối lệnh của nó.
  • Khối default: xử lý mọi giá trị month không nằm trong khoảng 1 đến 12.

Ví dụ 3.2: switch với enum

Sử dụng enum (kiểu dữ liệu liệt kê) với switch giúp code rõ ràng và dễ quản lý hơn khi làm việc với các tập hợp giá trị có tên.

#include <iostream>

// Định nghĩa kiểu enum cho các ngày trong tuần
enum class Day {
    Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
};

int main() {
    Day today = Day::Wednesday;

    switch (today) {
        case Day::Sunday:
            cout << "Hom nay la Chu Nhat. Chuc ban cuoi tuan vui ve!" << endl;
            break;
        case Day::Saturday:
            cout << "Hom nay la Thu Bay. Chuc ban cuoi tuan vui ve!" << endl;
            break;
        case Day::Monday:
        case Day::Tuesday:
        case Day::Wednesday:
        case Day::Thursday:
        case Day::Friday:
            cout << "Hom nay la ngay trong tuan. Co len nhe!" << endl;
            break;
        // Khong can default o day neu chung ta chac chan bien thuoc enum
        // Nhung them default co the giup debug
        default:
             cout << "Gia tri ngay khong hop le." << endl;
             break;
    }

    return 0;
}

Giải thích:

  • Chúng ta định nghĩa một enum class Day với các tên gọi cho các ngày trong tuần.
  • Biến today có kiểu Day.
  • Trong switch, chúng ta so sánh today với các giá trị hằng của enum (ví dụ: Day::Wednesday). Điều này giúp code dễ đọc hơn rất nhiều so với việc sử dụng các số nguyên magic number (ví dụ: 0 cho Chủ Nhật, 1 cho Thứ Hai...).
  • Tương tự ví dụ trước, chúng ta gom các ngày trong tuần lại để có cùng thông báo.

Sử dụng switch thường thích hợp hơn một chuỗi if-else if dài khi bạn so sánh một biến với nhiều giá trị hằng rời rạc. Nó thường rõ ràng hơn và compiler có thể tối ưu tốt hơn trong một số trường hợp. Tuy nhiên, nếu bạn cần kiểm tra các điều kiện phức tạp hơn, các khoảng giá trị, hoặc kết hợp nhiều biến, thì if/else if là lựa chọn phù hợp hơn.

4. Toán tử Điều kiện (Conditional Operator ? :)

Toán tử điều kiện, hay còn gọi là toán tử ba ngôi (ternary operator), là một cách viết tắt ngắn gọn cho câu lệnh if/else đơn giản mà kết quả là việc gán một giá trị hoặc trả về một biểu thức. Cú pháp của nó là:

điều_kiện ? giá_trị_nếu_đúng : giá_trị_nếu_sai;

Ví dụ 4.1: Xác định số lớn hơn

#include <iostream>

int main() {
    int a = 15;
    int b = 20;

    // Sử dụng toán tử điều kiện để tìm giá trị lớn nhất
    int max_value = (a > b) ? a : b;

    cout << "Gia tri lon nhat giua " << a << " va " << b << " la: " << max_value << endl;

    return 0;
}

Giải thích:

  • a > bđiều_kiện.
  • Nếu a > b đúng (true), biểu thức toán tử điều kiện sẽ trả về a.
  • Nếu a > b sai (false), biểu thức sẽ trả về b.
  • Giá trị được trả về sau đó được gán cho biến max_value.

Đây tương đương với:

int max_value;
if (a > b) {
    max_value = a;
} else {
    max_value = b;
}

Nhưng rõ ràng là ngắn gọn hơn nhiều!

Ví dụ 4.2: Gán chuỗi dựa trên điều kiện

#include <iostream>
#include <string>

int main() {
    int number = 7;

    // Sử dụng toán tử điều kiện để gán chuỗi
    string ket_qua = (number % 2 == 0) ? "so chan" : "so le";

    cout << number << " la " << ket_qua << endl;

    return 0;
}

Giải thích:

  • Điều kiện number % 2 == 0 kiểm tra xem number có chia hết cho 2 hay không.
  • Nếu đúng, chuỗi "so chan" được chọn.
  • Nếu sai, chuỗi "so le" được chọn.
  • Chuỗi được chọn được gán vào biến ket_qua.

Toán tử điều kiện là một công cụ tuyệt vời để viết code ngắn gọn cho các quyết định đơn giản. Tuy nhiên, nó chỉ nên được dùng cho các trường hợp đơn giản. Nếu điều kiện hoặc giá trị trả về quá phức tạp, việc sử dụng if/else truyền thống sẽ giúp code dễ đọc hơn rất nhiều. Đừng cố gắng lồng toán tử điều kiện vào nhau một cách phức tạp, nó sẽ trở thành "ác mộng" cho người đọc code!

5. Lời khuyên khi thực hành rẽ nhánh nâng cao

Bạn đã thấy các cách khác nhau để xử lý các tình huống rẽ nhánh phức tạp hơn. Dưới đây là một vài lời khuyên để bạn thực hành hiệu quả:

  • Chọn đúng công cụ:
    • Sử dụng if/else if cho các điều kiện phức tạp, các khoảng giá trị, hoặc khi thứ tự kiểm tra là quan trọng.
    • Sử dụng switch khi bạn so sánh một biến với nhiều giá trị hằng rời rạc.
    • Sử dụng toán tử điều kiện ? : cho các quyết định đơn giản, thường là để gán giá trị.
  • Ưu tiên khả năng đọc code: Đừng ngần ngại sử dụng dấu ngoặc đơn () trong các điều kiện phức hợp. Tránh lồng if quá sâu. Code rõ ràng quan trọng hơn code quá "thông minh" hoặc quá ngắn gọn đến mức khó hiểu.
  • Sử dụng default trong switch: Điều này giúp chương trình của bạn mạnh mẽ hơn trước các đầu vào không mong muốn.
  • Thực hành, thực hành, thực hành: Cách tốt nhất để nắm vững các khái niệm này là áp dụng chúng vào các bài toán nhỏ.

Hãy thử thách bản thân với một số bài tập nhỏ để củng cố kiến thức:

  1. Viết chương trình kiểm tra xem một năm có phải là năm nhuận không. Năm nhuận là năm chia hết cho 400, hoặc chia hết cho 4 nhưng không chia hết cho 100. (Sử dụng toán tử logic!)
  2. Viết chương trình yêu cầu người dùng nhập điểm số (từ 0-100) và in ra xếp loại (A, B, C, D, F) dựa trên thang điểm nhất định. (Sử dụng if-else if).
  3. Viết chương trình yêu cầu người dùng nhập một ký tự và kiểm tra xem đó là nguyên âm (a, e, i, o, u - không phân biệt hoa thường) hay phụ âm. (Sử dụng switch hoặc if với toán tử ||).

Bài tập ví dụ: C++ Bài 2.A5: Chia hết

Cho hai số \(a\) và \(b\). Xác định trong hai số trên, có một số chia hết cho số còn lại hay không.

INPUT FORMAT

Đầu vào gồm hai số nguyên dương \(a\) và \(b (0 < a, b \leq 10^4)\);

OUTPUT FORMAT

In ra một dòng duy nhất là yes nếu hai số chia hết cho nhau, ngược lại in ra no.

Ví dụ 1:

Input
6 3
Ouput
yes

Ví dụ 2:

Input
6 4
Output
no

Giải thích ví dụ mẫu:

  • Ví dụ 1: 6 chia hết cho 3 và ngược lại, nên kết quả là yes.
  • Ví dụ 2: 6 không chia hết cho 4 và 4 không chia hết cho 6, nên kết quả là no. <br>
  1. Bao gồm thư viện cần thiết: Để thực hiện nhập và xuất dữ liệu, bạn cần bao gồm thư viện iostream.
  2. Khai báo biến: Cần hai biến kiểu số nguyên để lưu trữ hai số ab từ đầu vào. Kiểu int là phù hợp vì giới hạn của ab10^4.
  3. Đọc dữ liệu: Sử dụng cin để đọc giá trị của hai số ab từ đầu vào chuẩn.
  4. Kiểm tra điều kiện: Bài toán yêu cầu kiểm tra xem một trong hai số có chia hết cho số còn lại hay không.
    • Để kiểm tra xem a có chia hết cho b hay không, sử dụng toán tử modulo %. Điều kiện là a % b == 0.
    • Để kiểm tra xem b có chia hết cho a hay không, điều kiện là b % a == 0.
    • Vì chỉ cần một trong hai điều kiện trên đúng, bạn sẽ kết hợp chúng bằng toán tử logic || (OR). Điều kiện kiểm tra tổng quát sẽ là (a % b == 0) || (b % a == 0).
  5. Xuất kết quả:
    • Sử dụng câu lệnh điều kiện if. Nếu điều kiện kiểm tra ở bước 4 là đúng, in ra dòng chữ yes bằng cout.
    • Ngược lại (trong nhánh else), in ra dòng chữ no bằng cout.
    • Đảm bảo thêm ký tự xuống dòng sau khi in (endl hoặc '\n') để đáp ứng định dạng đầu ra.
  6. Cấu trúc chương trình: Đặt tất cả các bước trên vào hàm int main() tiêu chuẩn của C++.

Tóm lại, bạn sẽ đọc hai số, dùng toán tử modulo để kiểm tra hai khả năng chia hết, kết hợp chúng bằng OR, và dùng if/else để in ra kết quả tương ứng.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.