Chuyển tới nội dung chính

Rẽ nhánh: if / else

Chương trình không phải lúc nào cũng chạy thẳng một mạch. Cuộc sống đầy tình huống kiểu "nếu trời mưa thì mang ô, không thì thôi", và phần mềm cũng vậy: nếu tài khoản đủ tiền thì cho rút, nếu điểm từ 5 trở lên thì đậu, nếu số chia bằng 0 thì báo lỗi. Rẽ nhánh là công cụ để chương trình chọn làm việc này hay việc kia tùy theo một điều kiện đúng hay sai.

Điều kiện trong C++ là một biểu thức cho ra true hoặc false. Ta dựng nó từ các phép so sánh (>, <, ==...) rồi ghép lại bằng các phép logic (&&, ||, !). Bài này đi từ if đơn giản nhất, tới if-else, chuỗi else if bậc thang, rồi switch-case, toán tử điều kiện ?:, cách viết biểu thức logic cho đúng độ ưu tiên, và cái bẫy kinh điển gõ nhầm = thành ==.

Hãy thử hình dung một máy rút tiền tự động không có rẽ nhánh: nó nhả tiền cho mọi người, kể cả người nhập sai mã PIN, kể cả khi tài khoản rỗng. Phần lớn giá trị của phần mềm nằm ở chỗ nó biết từ chối đúng lúc. Rẽ nhánh cũng là nơi bạn phải tập một kỹ năng khác hẳn việc gõ mã: liệt kê cho đủ các trường hợp. Bài toán xếp loại học lực trông đơn giản, nhưng bỏ sót ranh giới 5,0 thì chương trình vẫn chạy, chỉ là chạy sai, và trình biên dịch không bao giờ nhắc bạn chuyện đó.

Quy ước: các đoạn mã dưới đây ngầm hiểu đã có #include <iostream>using namespace std; ở đầu file.

Ý tưởng cốt lõi

  • if (điều_kiện) { ... } else { ... }: nếu điều kiện đúng chạy nhánh trên, ngược lại chạy nhánh else.
  • Điều kiện là biểu thức bool: >=, <=, ==, !=, kết hợp bằng && (và), || (hoặc).
  • Có thể nối nhiều mức bằng else if.

Thử ngay: xem rẽ nhánh chạy từng bước

Chọn nhánh theo điều kiện C++
1int main() {
2 int diem = 7;
3 if (diem >= 8)
4 cout << "Gioi";
5 else if (diem >= 5)
6 cout << "Dat";
7 else
8 cout << "Truot";
9}
Ngăn xếp stack
main()
diem= 7
Bộ nhớ động heap
(trống)
diem = 7.
1/4

if đơn: làm hoặc bỏ qua

Dạng đơn giản nhất là if không có else: nếu điều kiện đúng thì chạy khối lệnh, nếu sai thì bỏ qua và đi tiếp. Không có "việc kia", chỉ có làm hoặc không làm.

int tuoi = 20;
if (tuoi >= 18) {
cout << "Du tuoi\n"; // chi chay khi dieu kien dung
}
cout << "Ket thuc\n"; // dong nay luon chay

Khi khối chỉ có một lệnh, bạn có thể bỏ cặp ngoặc nhọn, nhưng đây là nguồn của nhiều lỗi khó thấy. Ngoặc nhọn chỉ ôm đúng lệnh đầu tiên; lệnh thứ hai sẽ luôn chạy dù điều kiện sai. Vì thế người mới nên tập thói quen luôn đặt ngoặc nhọn cho rõ ràng.

double diem = 4.0;
// NGUY HIEM: khong co ngoac nhon
if (diem >= 5)
cout << "Dat\n";
cout << "Chuc mung\n"; // dong nay LUON chay, khong thuoc if!

if-else: chọn một trong hai

Thêm else để xử lý trường hợp còn lại. Điều kiện đúng thì chạy nhánh trên, sai thì chạy nhánh else. Đúng một trong hai nhánh được chạy, không bao giờ cả hai.

int n;
cin >> n;
if (n % 2 == 0) {
cout << "Chan\n";
} else {
cout << "Le\n";
}
Dấu chấm phẩy lạc sau if

Viết if (n > 0); với một dấu chấm phẩy ngay sau ngoặc là lỗi cú pháp hợp lệ nhưng vô nghĩa. Dấu chấm phẩy đó chính là thân của if, một lệnh rỗng. Khối lệnh phía dưới trở thành một khối độc lập và luôn chạy bất kể điều kiện. Trình biên dịch không báo lỗi, nhiều lắm chỉ cảnh báo, nên hãy tự soi dòng if mỗi khi nhánh của bạn chạy sai một cách khó hiểu.

if lồng nhau: điều kiện bên trong điều kiện

Thân của một if có thể chứa một if khác. Ta dùng cách này khi điều kiện thứ hai chỉ có ý nghĩa sau khi điều kiện thứ nhất đã đúng. Ví dụ xét một sinh viên: trước hết phải đủ điều kiện dự thi, rồi trong số đủ điều kiện mới xét đậu hay trượt. Hai câu hỏi không ngang hàng, câu sau phụ thuộc câu trước, nên lồng nhau là cách diễn đạt trung thực nhất.

double diem = 6.5;
bool duDieuKien = true;

if (duDieuKien) {
if (diem >= 5.0) {
cout << "Dat\n";
} else {
cout << "Khong dat\n";
}
} else {
cout << "Cam thi\n"; // khong xet diem nua
}

Cần phân biệt rõ với chuỗi else if. Bậc thang else if dùng cho các trường hợp ngang hàng và loại trừ nhau, chỉ một nhánh được chọn. if lồng dùng cho các câu hỏi có thứ bậc, câu trong chỉ được hỏi khi câu ngoài đã đúng.

Lồng quá sâu là dấu hiệu xấu: ba tầng trở lên thì mắt người rất khó theo. Mẹo thường dùng là thoát sớm, kiểm tra trường hợp loại bỏ trước rồi kết thúc luôn, phần còn lại của hàm không cần thụt vào nữa.

#include <iostream>
using namespace std;

void xetTuyen(bool duDieuKien, double diem) {
if (!duDieuKien) { // loai bo som
cout << "Cam thi\n";
return; // thoat, khong lang phi mot tang thut le
}
if (diem >= 5.0) {
cout << "Dat\n";
} else {
cout << "Khong dat\n";
}
}

int main() {
xetTuyen(true, 6.5);
xetTuyen(false, 9.0);
return 0;
}

Hai đoạn mã trên cho cùng kết quả, nhưng đoạn sau phẳng hơn một tầng. Với hàm dài và nhiều điều kiện tiên quyết, khoảng cách đó tích lại thành khác biệt lớn.

else if bậc thang: nhiều mức

Khi có nhiều hơn hai trường hợp, nối các else if thành một bậc thang. Máy kiểm tra lần lượt từ trên xuống, gặp điều kiện đúng đầu tiên thì chạy nhánh đó rồi bỏ hết phần còn lại. Nhánh else cuối bắt mọi trường hợp không khớp điều kiện nào phía trên.

double diem = 7.0; // diem le nen phai la so thuc, khong phai int
if (diem >= 8) {
cout << "Gioi\n";
} else if (diem >= 6.5) {
cout << "Kha\n";
} else if (diem >= 5) {
cout << "Trung binh\n";
} else {
cout << "Yeu\n";
}

Thứ tự rất quan trọng. Vì bậc thang dừng ở điều kiện đúng đầu tiên, ta xếp từ chặt tới lỏng (từ điểm cao xuống thấp). Nếu đảo ngược, đặt diem >= 5 lên trên cùng, thì điểm 9 cũng khớp ngay dòng đầu và in nhầm "Trung binh".

Biểu thức logic và độ ưu tiên

Điều kiện thường ghép nhiều mệnh đề bằng các phép logic:

  • && (và): đúng khi cả hai vế đều đúng.
  • || (hoặc): đúng khi ít nhất một vế đúng.
  • ! (phủ định): đảo đúng thành sai và ngược lại.
int tuoi = 25;
bool coVe = true;
if (tuoi >= 18 && coVe) {
cout << "Duoc vao\n"; // can ca hai dieu kien
}
if (tuoi < 6 || tuoi >= 60) {
cout << "Duoc mien phi\n"; // chi can mot trong hai
}

Phép so sánh có độ ưu tiên cao hơn &&, và && cao hơn ||. Khi nghi ngờ, cứ thêm ngoặc để ý rõ ràng, ví dụ (a > 0) && (b > 0 || c > 0). Ngoặc vừa tránh sai vừa giúp người đọc hiểu nhanh.

Một tiện lợi nữa: C++ tính logic theo kiểu rút gọn (short-circuit). Với a && b, nếu a đã sai thì b không được tính; với a || b, nếu a đã đúng thì b bị bỏ qua. Nhờ đó ta có thể viết if (n != 0 && x / n > 2) mà không sợ chia cho 0, vì khi n == 0 thì vế chia không hề chạy.

Tính rút gọn không chỉ là chuyện tiện tay, nó quyết định thứ tự các mệnh đề. Vế bảo vệ phải đứng trước vế nguy hiểm. Viết if (x / n > 2 && n != 0) là sai hoàn toàn, vì phép chia được tính trước khi kịp kiểm tra n. Quy tắc thực dụng: mệnh đề nào rẻ và có tính chặn thì đặt lên đầu, mệnh đề nào đắt hoặc rủi ro thì đặt sau.

Một lỗi khác rất hay gặp là viết điều kiện kép theo lối toán học. Trong toán ta viết 0 <= x <= 100, nhưng C++ hiểu câu đó theo cách hoàn toàn khác: nó tính 0 <= x trước, ra true hoặc false, tức là số 1 hoặc số 0, rồi đem chính con số đó so tiếp với 100. Mà cả 1 lẫn 0 đều nhỏ hơn 100, nên vế sau luôn đúng. Kết quả là điều kiện luôn đúng với mọi x, kể cả x bằng một triệu. Cách viết đúng là tách thành hai mệnh đề nối bằng &&.

int x = 1000000;
// SAI: luon dung, khong loc duoc gi
// if (0 <= x <= 100) cout << "Hop le\n";

// DUNG: hai menh de noi bang &&
if (0 <= x && x <= 100) {
cout << "Hop le\n";
} else {
cout << "Ngoai khoang\n"; // dong nay moi la ket qua dung
}

So sánh số thực: đừng dùng ==

Số thực trong máy tính được lưu ở dạng nhị phân với số bit hữu hạn, nên phần lớn số thập phân quen thuộc không biểu diễn được chính xác. Giá trị 0,1 trong máy thực ra là một số rất gần 0,1 chứ không đúng bằng. Cộng ba lần cái "rất gần" ấy lại, ta được một số rất gần 0,3 nhưng lệch một chút. Vì thế phép so sánh bằng trên số thực có thể cho kết quả trái với trực giác.

#include <iostream>
#include <cmath> // fabs
using namespace std;

int main() {
double a = 0.1 + 0.1 + 0.1;
double b = 0.3;

if (a == b) {
cout << "Bang nhau\n"; // thuong KHONG in ra
} else {
cout << "Khac nhau\n"; // ket qua thuc te
}

const double EPS = 1e-9; // sai so cho phep
if (fabs(a - b) < EPS) {
cout << "Coi nhu bang nhau\n"; // dong nay moi dung
}
return 0;
}

Cách làm chuẩn là so sánh khoảng cách giữa hai số với một ngưỡng sai số nhỏ, thường gọi là epsilon. Hàm fabs trong thư viện <cmath> cho giá trị tuyệt đối của một số thực. Nếu fabs(a - b) nhỏ hơn epsilon thì ta coi hai số bằng nhau. Cùng lý do đó, kiểm tra một số thực có bằng 0 hay không nên viết if (fabs(x) < EPS) thay vì if (x == 0.0). Riêng phép so sánh lớn hơn hoặc nhỏ hơn vẫn dùng bình thường.

Hai cái bẫy khi so sánh

Một, == trên double hoặc float là không đáng tin, hãy so qua epsilon. Hai, với string trong C++ thì == lại hoạt động đúng như mong đợi vì nó so nội dung chuỗi, khác hẳn C nơi phải gọi strcmp. Nói cách khác, quy tắc tránh == chỉ áp cho số thực.

switch-case: rẽ theo một giá trị rời rạc

Khi cần chọn nhánh dựa trên giá trị cụ thể của một biến nguyên hoặc ký tự, switch gọn hơn chuỗi if-else. Mỗi case ứng với một giá trị; break kết thúc nhánh; default bắt các giá trị còn lại.

int thu;
cin >> thu;
switch (thu) {
case 7:
cout << "Thu bay\n";
break;
case 1:
cout << "Chu nhat\n";
break;
default:
cout << "Ngay trong tuan\n";
}

Nhớ break sau mỗi case. Quên break, chương trình sẽ rơi xuyên (fall-through) chạy luôn các case phía dưới. Đôi khi ta cố ý gộp nhiều giá trị cùng xử lý bằng cách xếp các case liền nhau không có break ở giữa.

char c;
cin >> c;
switch (c) {
case 'a': case 'e': case 'i': case 'o': case 'u':
cout << "Nguyen am\n"; // gom 5 case, cung mot xu ly
break;
default:
cout << "Phu am\n";
}

Lưu ý switch chỉ so khớp bằng nhau trên kiểu nguyên hoặc char, không dùng được cho khoảng như diem >= 5 hay cho string, double. Những trường hợp đó vẫn phải dùng if-else.

Quên break là lỗi im lặng

Trình biên dịch coi việc rơi xuyên là hợp lệ, vì đôi khi lập trình viên cố ý dùng nó. Do đó quên break không sinh ra lỗi biên dịch, chỉ sinh ra kết quả sai. Nếu chương trình của bạn in ra nhiều dòng hơn dự kiến, hãy soi lại từng case xem có break chưa. Khi bạn thật sự muốn rơi xuyên, hãy ghi một dòng chú thích nói rõ ý định để người đọc sau không tưởng là bạn quên.

Nên chọn switch hay bậc thang if-else? Bảng dưới tóm tắt cách quyết định.

Tình huốngNên dùngVì sao
So một biến nguyên với nhiều giá trị rời rạcswitchgọn, ý đồ rõ, dễ thêm nhánh
Điều kiện theo khoảng, ví dụ diem >= 8if-elseswitch chỉ so bằng, không xét được khoảng
Điều kiện ghép nhiều biến bằng &&if-elseswitch chỉ xét một biểu thức
Rẽ theo double hoặc stringif-elseswitch không nhận hai kiểu này
Nhiều giá trị cùng chung một xử lýswitchxếp các case liền nhau là xong

Toán tử điều kiện ?:

Khi chỉ cần chọn một trong hai giá trị (không phải hai khối lệnh dài), toán tử ba ngôi điều_kiện ? giá_trị_nếu_đúng : giá_trị_nếu_sai cho cách viết gọn.

int a, b;
cin >> a >> b;
int lon = (a > b) ? a : b; // gan gia tri lon hon
cout << (a % 2 == 0 ? "Chan" : "Le") << "\n"; // in truc tiep

Dùng ?: cho việc ngắn gọn thì đẹp, nhưng đừng lồng nhiều tầng vào nhau vì sẽ rất khó đọc; lúc đó if-else rõ hơn.

Mã nguồn

Gom lại thành một chương trình chạy được, đây là bậc thang else if mà trình mô phỏng ở đầu bài đã đi qua từng nhịp. Đọc lại lúc này, mỗi dòng đã có đủ lý do đứng ở đó.

#include <iostream>
using namespace std;

int main() {
int diem = 7;
if (diem >= 8)
cout << "Gioi";
else if (diem >= 5)
cout << "Dat";
else
cout << "Truot";
}

Tự kiểm tra

Nếu diem = 5 thì in ra gì?

In "Dat". Vì 5 >= 8 sai, nhưng 5 >= 5 đúng nên rơi vào nhánh else if. Lưu ý >= bao gồm cả bằng; nếu viết > 5 thì diem = 5 sẽ trượt xuống else.

Nhầm = với ==

Đây là lỗi kinh điển nhất khi rẽ nhánh. ==so sánh, còn =gán. Viết if (x = 5) không báo lỗi cú pháp: nó gán 5 cho x rồi lấy chính giá trị 5 làm điều kiện, mà 5 khác 0 nên luôn đúng. Kết quả là nhánh if chạy bất kể x ban đầu là gì. Muốn kiểm tra bằng nhau phải viết if (x == 5).

Bài 1: đọc một số nguyên, in "Duong", "Am" hoặc "Khong" tùy dấu của nó.
#include <iostream>
using namespace std;

int main() {
int n;
cin >> n;
if (n > 0) {
cout << "Duong\n";
} else if (n < 0) {
cout << "Am\n";
} else {
cout << "Khong\n";
}
return 0;
}

Ba trường hợp loại trừ nhau nên dùng bậc thang if / else if / else là gọn nhất. Nhánh else cuối bắt đúng trường hợp n == 0.

Bài 2: đọc ba cạnh a, b, c, kiểm tra chúng có tạo thành tam giác hay không.
#include <iostream>
using namespace std;

int main() {
int a, b, c;
cin >> a >> b >> c;
// ba canh duong va tong hai canh bat ky lon hon canh con lai
if (a > 0 && b > 0 && c > 0 &&
a + b > c && a + c > b && b + c > a) {
cout << "YES\n";
} else {
cout << "NO\n";
}
return 0;
}

Điều kiện ghép nhiều mệnh đề bằng &&, tất cả phải đúng thì mới là tam giác. Nhờ tính rút gọn, nếu một cạnh không dương thì các phép so sánh sau không cần tính.

Bài 3: đọc một ký tự phép toán (+, -, *, /) và hai số, in kết quả bằng switch.
#include <iostream>
using namespace std;

int main() {
char op;
double a, b;
cin >> op >> a >> b;
switch (op) {
case '+': cout << a + b << "\n"; break;
case '-': cout << a - b << "\n"; break;
case '*': cout << a * b << "\n"; break;
case '/':
if (b != 0) cout << a / b << "\n";
else cout << "Khong chia cho 0\n";
break;
default: cout << "Phep toan khong hop le\n";
}
return 0;
}

switch khớp đúng ký tự phép toán. Nhớ break sau mỗi case để không rơi xuyên xuống nhánh dưới, và kiểm tra chia cho 0 trước khi thực hiện phép chia.

Bài 4: đọc một số thực x, kiểm tra x có bằng 0,3 hay không cho đúng cách.
#include <iostream>
#include <cmath> // fabs
using namespace std;

int main() {
double x;
cin >> x;
const double EPS = 1e-9;

if (fabs(x - 0.3) < EPS) {
cout << "Bang 0.3\n";
} else if (x > 0.3) {
cout << "Lon hon 0.3\n";
} else {
cout << "Nho hon 0.3\n";
}
return 0;
}

Không dùng x == 0.3 vì số thực nhị phân hầu như không bao giờ khớp tuyệt đối. Ta so khoảng cách fabs(x - 0.3) với một epsilon nhỏ. Hai nhánh còn lại dùng phép so sánh lớn hơn và nhỏ hơn, vốn vẫn tin cậy được.

Bài 5: đọc năm, in ra năm đó có nhuận hay không.
#include <iostream>
using namespace std;

int main() {
int nam;
cin >> nam;
// nhuan: chia het cho 4, tru cac nam tron the ky khong chia het cho 400
bool nhuan = (nam % 4 == 0 && nam % 100 != 0) || (nam % 400 == 0);

if (nhuan) {
cout << "Nam nhuan\n";
} else {
cout << "Khong phai nam nhuan\n";
}
return 0;
}

Đây là bài tập kinh điển về độ ưu tiên. Vì && được tính trước ||, biểu thức trên vẫn đúng dù bỏ ngoặc, nhưng cặp ngoặc làm ý đồ hiện rõ ngay. Gán kết quả vào một biến bool có tên rõ nghĩa cũng là thói quen tốt, nó biến một dòng điều kiện rối thành hai dòng dễ đọc.

Câu hỏi tự kiểm

Kiểm tra nhanh: rẽ nhánh0/4 đúngchưa trả lời
  1. 1Câu if (x = 5) làm gì?
  2. 2Với x = 1000000, điều kiện if (0 <= x <= 100) cho kết quả gì?
  3. 3Trong switch, quên break sau một case dẫn tới điều gì?
  4. 4Trường hợp nào KHÔNG dùng được switch mà phải dùng if-else?

Tóm tắt

if đơn dùng khi chỉ có làm hoặc không làm. if-else chọn một trong hai. Bậc thang else if xử lý nhiều trường hợp ngang hàng và loại trừ nhau, kiểm tra lần lượt từ trên xuống nên thứ tự điều kiện phải đi từ chặt tới lỏng. if lồng nhau dành cho câu hỏi có thứ bậc, và khi lồng quá sâu thì mẹo thoát sớm giúp mã phẳng lại. switch gọn hơn hẳn khi so một biến nguyên hoặc ký tự với nhiều giá trị rời rạc, nhưng không dùng được cho khoảng, cho double hay cho string. Toán tử ?: dành cho việc chọn một trong hai giá trị ngắn gọn.

Về điều kiện, nhớ ba điều: &&|| tính theo lối rút gọn nên mệnh đề bảo vệ phải đứng trước mệnh đề nguy hiểm; điều kiện kép kiểu toán học 0 <= x <= 100 là bẫy, phải tách thành 0 <= x && x <= 100; số thực không so bằng == mà so khoảng cách với một epsilon nhỏ. Cuối cùng là ba lỗi im lặng mà trình biên dịch không chặn: gõ = thay vì ==, đặt dấu chấm phẩy ngay sau if, và quên break trong switch.

Ghi nhớ

if/else if/else chọn đúng một nhánh: kiểm tra lần lượt từ trên xuống, gặp điều kiện đúng đầu tiên thì chạy nhánh đó rồi bỏ qua phần còn lại.