Bài 2.2: Cấu trúc rẽ nhánh switch-case trong C++

Chào mừng các bạn quay trở lại với loạt bài viết về Lập trình C++ cơ bản trên FullhouseDev!

Trong các bài học trước, chúng ta đã làm quen với cấu trúc rẽ nhánh if-else mạnh mẽ, cho phép chương trình đưa ra quyết định dựa trên các điều kiện khác nhau. Hôm nay, chúng ta sẽ khám phá một công cụ rẽ nhánh khác, cực kỳ hữu ích khi bạn cần xử lý nhiều trường hợp cụ thể dựa trên giá trị của một biểu thức duy nhất. Đó chính là cấu trúc switch-case.

Vậy switch-case là gì và tại sao chúng ta lại cần nó khi đã có if-else? Hãy cùng tìm hiểu nhé!

switch-case là gì?

Cấu trúc switch-case trong C++ được thiết kế để làm cho việc kiểm tra bằng nhau một biến hoặc một biểu thức với nhiều giá trị hằng khác nhau trở nên rõ ràng và dễ đọc hơn so với việc sử dụng chuỗi dài các câu lệnh if-else if-else.

Hãy tưởng tượng bạn cần viết một chương trình yêu cầu người dùng nhập một số từ 1 đến 7 để hiển thị ngày trong tuần. Bạn có thể dùng if-else if-else, nhưng với switch-case, code sẽ trực quan hơn rất nhiều.

Cú pháp cơ bản của switch-case

Cấu trúc switch-case có cú pháp như sau:

switch (expression) {
    case constant_value1:
        // Khối lệnh thực thi nếu expression == constant_value1
        // ...
        break; // <-- RẤT QUAN TRỌNG!
    case constant_value2:
        // Khối lệnh thực thi nếu expression == constant_value2
        // ...
        break; // <-- RẤT QUAN TRỌNG!
    // ... Có thể có nhiều case khác
    case constant_valueN:
        // Khối lệnh thực thi nếu expression == constant_valueN
        // ...
        break;
    default:
        // Khối lệnh thực thi nếu expression không khớp với bất kỳ case nào
        // ...
        // break; // break ở đây thường không cần thiết nhưng cũng không hại gì
}

Hãy cùng phân tích các thành phần chính:

  1. switch (expression): Đây là điểm bắt đầu của cấu trúc. expression là một biểu thức mà giá trị của nó sẽ được so sánh với các giá trị trong các case phía dưới. Quan trọng: expression phải có kiểu dữ liệu là kiểu nguyên (integral type), ví dụ như int, char, enum,... hoặc các kiểu dữ liệu có thể được chuyển đổi ngầm định sang kiểu nguyên.
  2. case constant_valueX:: Đây là một nhãn (label). constant_valueX phải là một hằng số nguyên (constant integral expression). Chương trình sẽ nhảy đến khối lệnh ngay sau nhãn case constant_valueX nếu giá trị của expression khớp chính xác với constant_valueX.
  3. break;: Đây là một từ khóa cực kỳ quan trọng. Khi câu lệnh break; được thực thi, chương trình sẽ thoát ra khỏi toàn bộ khối switch, tiếp tục thực thi các lệnh ngay sau dấu đóng ngoặc nhọn } của switch. Nếu thiếu break, chương trình sẽ tiếp tục thực thi các lệnh trong các case tiếp theo (hiện tượng "fall-through"), cho đến khi gặp break hoặc kết thúc khối switch. Chúng ta sẽ nói kỹ hơn về điều này sau.
  4. default:: Đây là nhãn tùy chọn. Khối lệnh sau default sẽ được thực thi khi giá trị của expression không khớp với bất kỳ constant_value nào trong các case đã liệt kê. default thường được đặt ở cuối khối switch, nhưng về mặt cú pháp, nó có thể đặt ở bất kỳ đâu.

switch-case Hoạt động như thế nào?

Khi chương trình gặp một câu lệnh switch:

  1. Biểu thức expression trong dấu ngoặc đơn switch(...) được tính toán giá trị.
  2. Giá trị này lần lượt được so sánh với các constant_value trong từng case.
  3. Ngay khi tìm thấy một case có giá trị khớp, chương trình sẽ nhảy (jump) đến dòng code ngay sau nhãn case đó và bắt đầu thực thi.
  4. Chương trình sẽ tiếp tục thực thi tất cả các câu lệnh từ điểm nhảy đó trở xuống, bỏ qua việc kiểm tra các nhãn case tiếp theo, cho đến khi:
    • Gặp câu lệnh break;.
    • Hoặc kết thúc khối switch.
    • Hoặc gặp câu lệnh nhảy khác như return.
  5. Nếu không có case nào khớp với giá trị của expression, và có nhãn default, chương trình sẽ nhảy đến khối lệnh sau default.
  6. Nếu không có case nào khớp và cũng không có nhãn default, chương trình sẽ không làm gì cả bên trong khối switch và tiếp tục thực thi lệnh ngay sau khối switch.

Tầm quan trọng của break (và hiện tượng Fall-through)

Hãy nhấn mạnh lại: Từ khóa breaklinh hồn của hầu hết các trường hợp sử dụng switch-case thông thường. Nó giúp bạn chỉ thực thi khối lệnh tương ứng với case khớp và sau đó thoát ra.

Nếu bạn quên đặt break; ở cuối một khối case, chương trình sẽ không thoát khỏi switch sau khi thực thi khối lệnh đó. Thay vào đó, nó sẽ "rơi xuyên qua" (fall-through) và tiếp tục thực thi các lệnh trong case tiếp theo (và tiếp theo nữa, cho đến khi gặp break hoặc kết thúc switch).

Hiện tượng "fall-through" này có thể có chủ đích trong một số trường hợp nhất định (ví dụ: để gộp nhiều case thực hiện cùng một hành động), nhưng thông thường nó là nguyên nhân của lỗi logic không mong muốn.

Hãy xem ví dụ về fall-through:

#include <iostream>

int main() {
    int lua_chon = 1;

    cout << "Ban da chon so " << lua_chon << ".\n";

    switch (lua_chon) {
        case 1:
            cout << "Executing case 1...\n";
            // OH NO! Quên break ở đây!
        case 2:
            cout << "Executing case 2...\n";
            break; // Gap break, thoat switch
        case 3:
            cout << "Executing case 3...\n";
            break;
        default:
            cout << "Executing default...\n";
    }

    cout << "Ket thuc switch.\n";

    return 0;
}

Giải thích code:

  • Biến lua_chon được gán giá trị 1.
  • Câu lệnh switch (lua_chon) so sánh giá trị 1 với các case.
  • Nó khớp với case 1:. Chương trình nhảy đến đó.
  • In ra "Executing case 1...".
  • Không có break;! Chương trình không thoát, mà tiếp tục thực thi các lệnh trong case 2.
  • In ra "Executing case 2...".
  • Gặp break; trong case 2. Chương trình thoát khỏi toàn bộ khối switch.
  • In ra "Ket thuc switch.".

Output của chương trình này sẽ là:

Ban da chon so 1.
Executing case 1...
Executing case 2...
Ket thuc switch.

Bạn thấy đấy, vì thiếu breakcase 1, code của case 2 cũng bị thực thi! Đây là một lỗi rất phổ biến khi mới làm quen với switch-case. Hãy luôn nhớ đặt break; ở cuối mỗi case trừ khi bạn thực sự muốn fall-through có chủ đích.

Sử dụng default Case

Nhãn default hoạt động như một "ngăn chứa" cho tất cả các trường hợp mà expression không khớp với bất kỳ constant_value nào trong các case đã liệt kê. Nó tương tự như mệnh đề else trong cấu trúc if-else if-else.

Việc sử dụng defaultmột thực hành tốt vì nó giúp xử lý các giá trị không mong muốn hoặc không hợp lệ, làm cho chương trình của bạn robust hơn.

Ví dụ với default:

#include <iostream>

int main() {
    int ngay_trong_tuan;

    cout << "Nhap mot so (1-7): ";
    cin >> ngay_trong_tuan;

    switch (ngay_trong_tuan) {
        case 1:
            cout << "Hom nay la Chu Nhat.\n";
            break;
        case 2:
            cout << "Hom nay la Thu Hai.\n";
            break;
        case 3:
            cout << "Hom nay la Thu Ba.\n";
            break;
        case 4:
            cout << "Hom nay la Thu Tu.\n";
            break;
        case 5:
            cout << "Hom nay la Thu Nam.\n";
            break;
        case 6:
            cout << "Hom nay la Thu Sau.\n";
            break;
        case 7:
            cout << "Hom nay la Thu Bay.\n";
            break;
        default:
            cout << "So ban nhap khong hop le! Vui long nhap so tu 1 den 7.\n";
            // break; // break ở đây không cần thiết vì default là cuối cùng
    }

    return 0;
}

Giải thích code:

  • Chương trình yêu cầu người dùng nhập một số.
  • Giá trị nhập vào được đưa vào switch.
  • Nếu người dùng nhập 1, 2, ..., 7, chương trình sẽ in ra ngày tương ứng và thoát nhờ break;.
  • Nếu người dùng nhập một số khác (ví dụ: 0, 8, -5), không có case nào khớp.
  • Chương trình nhảy đến default và in ra thông báo lỗi.

Các Ví dụ Minh Họa Khác

Ví dụ 1: Phân loại Điểm số bằng char

switch-case không chỉ hoạt động với int, nó còn hoạt động tốt với charchar cũng là một kiểu nguyên.

#include <iostream>

int main() {
    char diem_chu;

    cout << "Nhap diem chu cua ban (A, B, C, D, F): ";
    cin >> diem_chu;

    switch (diem_chu) {
        case 'A':
            cout << "Xuat sac!\n";
            break;
        case 'B':
            cout << "Gioi!\n";
            break;
        case 'C':
            cout << "Kha.\n";
            break;
        case 'D':
            cout << "Trung binh.\n";
            break;
        case 'F':
            cout << "Truot.\n";
            break;
        default:
            cout << "Ky tu diem khong hop le.\n";
    }

    return 0;
}

Giải thích code:

  • Biến diem_chu kiểu char được sử dụng làm biểu thức trong switch.
  • Các nhãn case sử dụng các ký tự hằng ('A', 'B', v.v.) để so sánh.
  • Chương trình in ra kết quả tương ứng dựa trên điểm chữ được nhập.
Ví dụ 2: Sử dụng Fall-through Có Chủ Đích

Như đã đề cập, đôi khi bạn muốn nhiều case thực hiện cùng một khối lệnh. Thay vì lặp lại code, bạn có thể sử dụng fall-through bằng cách không đặt break; ở cuối các case đó.

Ví dụ: Phân loại xem một ký tự là nguyên âm hay phụ âm (đơn giản, không xét các trường hợp đặc biệt).

#include <iostream>

int main() {
    char ky_tu;

    cout << "Nhap mot ky tu chu cai: ";
    cin >> ky_tu;

    switch (ky_tu) {
        case 'a':
        case 'e':
        case 'i':
        case 'o':
        case 'u':
        case 'A': // Thêm cả trường hợp chữ hoa
        case 'E':
        case 'I':
        case 'O':
        case 'U':
            cout << ky_tu << " la mot nguyen am.\n";
            break; // Break sau khi đã kiểm tra tất cả nguyên âm
        default:
            cout << ky_tu << " la mot phu am (hoac khong phai chu cai).\n";
            // break; // break không cần thiết
    }

    return 0;
}

Giải thích code:

  • Nếu ky_tu là 'a', nó khớp với case 'a':. Không có break, nên chương trình fall-through xuống case 'e':, rồi 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'. Cuối cùng, nó gặp break; sau case 'U': và thực thi cout << ky_tu << " la mot nguyen am.\n"; một lần duy nhất.
  • Nếu ky_tu không khớp với bất kỳ nguyên âm nào, chương trình nhảy đến default và in ra thông báo đó là phụ âm (hoặc không phải chữ cái).
  • Cách này giúp tránh lặp lại câu lệnh cout << ky_tu << " la mot nguyen am.\n"; cho mỗi nguyên âm.

So sánh switch-case và if-else if-else

Khi nào nên dùng switch-case và khi nào nên dùng if-else if-else?

  • Sử dụng switch-case khi:

    • Bạn đang so sánh một biến duy nhất hoặc một biểu thức với nhiều giá trị hằng số nguyên (integral constants) khác nhau.
    • Code của bạn sẽ rõ ràng và dễ đọc hơn rất nhiều khi có nhiều hơn một vài trường hợp để kiểm tra sự bằng nhau.
  • Sử dụng if-else if-else khi:

    • Bạn cần kiểm tra các điều kiện phức tạp (ví dụ: x > 10 && y < 20).
    • Bạn cần kiểm tra các khoảng giá trị (ví dụ: diem >= 8.0 && diem < 9.0).
    • Bạn cần so sánh với các giá trị không phải hằng số nguyên (ví dụ: chuỗi string, số thực double).
    • Các điều kiện kiểm tra dựa trên nhiều biến khác nhau.

Nói cách khác, switch-case là một công cụ chuyên biệt, tối ưu cho việc rẽ nhánh dựa trên sự bằng nhau của một giá trị duy nhất với các hằng số, còn if-else if-else là cấu trúc rẽ nhánh tổng quát hơn, có thể xử lý mọi loại điều kiện.

Những Điều Cần Lưu Ý Khi Sử Dụng switch-case

  1. Kiểu dữ liệu: Biểu thức trong switch và các giá trị trong case phải là kiểu nguyên hoặc có thể chuyển đổi sang kiểu nguyên. Bạn không thể sử dụng trực tiếp float, double, string (trừ C++11+ với cách tiếp cận phức tạp hơn hoặc các thủ thuật, nhưng thường if-else hoặc cấu trúc khác sẽ phù hợp hơn) trong switch-case theo cách thông thường.
  2. Giá trị case phải là hằng: Giá trị sau case phải là một hằng số nguyên (một giá trị cố định được biết tại thời điểm biên dịch). Bạn không thể sử dụng một biến (case bien_x:) hoặc một biểu thức phức tạp (case a + b:) làm nhãn case.
  3. Tính duy nhất: Tất cả các giá trị trong các nhãn case của cùng một khối switch phải là duy nhất. Trình biên dịch sẽ báo lỗi nếu có hai case cùng giá trị.
  4. Khối lệnh trong case: Một case có thể chứa nhiều câu lệnh. Bạn không cần phải đặt các câu lệnh này trong dấu ngoặc nhọn {} trừ khi bạn muốn khai báo một biến cục bộ chỉ tồn tại trong case đó (nhưng ngay cả khi đó, tốt nhất nên đặt toàn bộ khối code trong {} để tránh lỗi). Tuy nhiên, đối với các case đơn giản chỉ có vài dòng, việc bỏ qua {} là phổ biến.

Bài tập ví dụ: C++ Bài 2.A2: Phương Trình Bậc Nhất

Hãy lập trình giải phương trình \(ax+b = 0\) với \(a\) và \(b\) nguyên nhập vào từ bàn phím. Kết quả làm tròn 2 chữ số thập phân.

INPUT FORMAT

Dòng đầu tiên chứa giá trị của hai số nguyên \(a, b\)

OUTPUT FORMAT

In ra kết quả của bài toán theo ví dụ bên dưới.

Ví dụ 1:

Input
0 1
Ouput
VO NGHIEM

Ví dụ 2:

Input
1 2
Output
PT CO NGHIEM
X = -2.00

Ví dụ 3:

Input
0 0
Output
VO SO NGHIEM

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

  • Ví dụ 1: Với a = 0b = 1, phương trình không có nghiệm, nên in VO NGHIEM.
  • Ví dụ 2: Với a = 1b = 2, phương trình có nghiệm -2.00, nên in PT CO NGHIEMX = -2.00.
  • Ví dụ 3: Với a = 0b = 0, phương trình có vô số nghiệm, nên in VO SO NGHIEM. <br>

Bài toán yêu cầu giải phương trình ax + b = 0 với ab là các số nguyên nhập từ bàn phím. Ta cần xét các trường hợp có thể xảy ra dựa vào giá trị của a:

  1. Trường hợp a khác 0 (a != 0):

    • Phương trình có dạng ax = -b.
    • Trong trường hợp này, phương trình có nghiệm duy nhất là x = -b / a.
    • ab là số nguyên, phép chia -b / a có thể cho kết quả không nguyên. Do đó, bạn cần thực hiện phép chia này với kiểu dữ liệu dấu phẩy động (như double) để có kết quả chính xác. Lưu ý ép kiểu (cast) ít nhất một trong hai toán hạng trước khi chia để tránh phép chia số nguyên.
    • Kết quả cần làm tròn đến 2 chữ số thập phân. Bạn sẽ cần sử dụng các công cụ định dạng output của C++.
    • In ra "PT CO NGHIEM" trên một dòng, sau đó in "X = " theo định dạng đã làm tròn.
  2. Trường hợp a bằng 0 (a == 0):

    • Khi a = 0, phương trình trở thành 0 * x + b = 0, tức là b = 0.
    • Xét tiếp giá trị của b:
      • Nếu b cũng bằng 0 (b == 0): Phương trình trở thành 0 = 0. Điều này đúng với mọi giá trị của x. Phương trình có vô số nghiệm.
        • In ra "VO SO NGHIEM".
      • Nếu b khác 0 (b != 0): Phương trình trở thành b = 0, nhưng ta đang xét trường hợp b != 0. Điều này là vô lý, không có giá trị x nào thỏa mãn. Phương trình vô nghiệm.
        • In ra "VO NGHIEM".

Hướng dẫn các bước thực hiện:

  1. Bao gồm các thư viện cần thiết: iostream cho việc nhập xuất và iomanip cho việc định dạng output (làm tròn số thập phân).
  2. Trong hàm main, khai báo hai biến kiểu int để lưu giá trị của ab.
  3. Sử dụng cin để đọc hai số nguyên ab từ input.
  4. Sử dụng cấu trúc điều khiển if-else if-else hoặc lồng if để xử lý các trường hợp:
    • Kiểm tra if (a == 0).
    • Bên trong khối if (a == 0), kiểm tra tiếp if (b == 0).
    • Sử dụng else cho trường hợp còn lại khi a == 0 (tức là b != 0).
    • Sử dụng else cho trường hợp a != 0.
  5. Trong từng nhánh của cấu trúc điều khiển, in ra kết quả tương ứng ("VO SO NGHIEM", "VO NGHIEM", hoặc nghiệm duy nhất).
  6. Khi in nghiệm duy nhất (a != 0), tính giá trị x bằng cách chia -b cho a. Đảm bảo sử dụng kiểu dữ liệu double cho phép chia (ví dụ: double x = static_cast<double>(-b) / a; hoặc đơn giản hơn double x = (double)-b / a;).
  7. Trước khi in giá trị x, thiết lập định dạng output cho cout bằng cách sử dụng fixedsetprecision(2) từ thư viện iomanip.
  8. In chuỗi "PT CO NGHIEM" rồi xuống dòng.
  9. In chuỗi "X = " rồi in giá trị x đã được định dạng.
  10. Kết thúc chương trình.

Lưu ý:

  • Sử dụng cout << ... << endl; hoặc cout << ... << '\n'; để xuống dòng sau mỗi thông báo hoặc giá trị in ra, đảm bảo đúng định dạng output.
  • Ưu tiên sử dụng các thành phần trong namespace std (cin, cout, fixed, setprecision).

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

Comments

There are no comments at the moment.