Chương 3: Cấu trúc điều khiển
Cấu trúc rẽ nhánh, vòng lặp và điều hướng luồng thực thi - những công cụ cốt lõi giúp chương trình có khả năng đưa ra quyết định thông minh và tự động hóa các tác vụ một cách hiệu quả.
1. Giới thiệu cấu trúc điều khiển
Trong , 1 chương trình sẽ bao gồm nhiều cấu trúc điều khiển khác nhau và cấu trúc điều khiển là tập hợp các câu lệnh quyết định chương trình được thực thi như thế nào.
1 cấu trúc điều khiển có thể có 1 hoặc nhiều chức năng sau:
- Cấu trúc tuần tự: thực hiện một chuỗi các phát biểu một cách tuần tự theo thứ tự xuất hiện của chúng trong chương trình.
- Cấu trúc chọn: phụ thuộc vào điều kiện của câu lệnh mà dữ liệu được đưa vào chương trình có được phép thực thi chương trình để giải quyết 1 vấn đề hay không.
- Cấu trúc lặp: dữ liệu được lặp đi lặp lại đến khi dữ liệu không thỏa điều kiện của cấu trúc nữa.
- Gọi hàm: gọi lại 1 hàm ngoài hàm chính để thực thi chức năng mà hàm chính cần để xử lí dữ liệu.
Phân loại cấu trúc điều khiển:
| Phân loại | Ý nghĩa | C++ |
|---|---|---|
| Câu lệnh điều kiện (conditional statements) | Tập các câu lệnh chỉ thực thi khi thỏa mãn điều kiện nào đó | if, else, switch |
| Câu lệnh nhảy (jumps) | Ra lệnh cho CPU bắt đầu chạy từ một nơi nào đó trong chương trình | goto, break, continue |
| Lời gọi hàm (Function calls) | Nhảy đến một vị trí nào đó trong chương trình để thực thi khối lệnh rồi quay trở lại chương trình chính | function calls, return |
| Vòng lặp (Loops) | Thực thi một/một khối lệnh nào đó lặp đi lặp lại miễn là vẫn thỏa điều kiện lặp | while, do-while, for, range-based for |
| Lệnh dừng (Halts) | Dừng chương trình | std::exit(), std::abort() |
| Lệnh xử lí trường hợp ngoại lệ (Exceptions) | Cấu trúc điều khiển đặc biệt dùng để bắt và xử lý các lỗi (ngoại lệ) phát sinh trong lúc chạy chương trình. | try, catch, throw |
2. Cấu trúc rẽ nhánh if, if-else, if-else if-else và switch-case.
2.1. Cấu trúc if
Loại cấu trúc điều khiển đầu tiên mà chúng ta tìm hiểu là cấu trúc rẽ nhánh dựa trên điều kiện (conditional statements).
Cấu trúc if (nếu) sẽ kiểm tra một biểu thức điều kiện. Nếu điều kiện đó được thỏa mãn (đúng), chương trình sẽ thực thi khối lệnh bên trong nó. Ngược lại, nếu điều kiện không thỏa mãn (sai), chương trình sẽ bỏ qua toàn bộ khối lệnh đó và đi tiếp.
Trong C++, kết quả của một phép toán so sánh (ví dụ: x > y) sẽ trả về giá trị 1 (nếu đúng) và 0 (nếu sai).
Cú pháp của if:
if (biểu_thức_điều_kiện) {
// khối lệnh
}
- Ví dụ:
Stdin:
Stdout:
Một điểm đặc biệt của C++ (so với nhiều ngôn ngữ khác) là biểu_thức_điều_kiện bên trong ngoặc if (...) không bắt buộc phải là một phép so sánh hay logic. Nó có thể là bất kỳ biểu thức nào, miễn là trả về một giá trị (một con số độc lập, biểu thức toán học, hay thậm chí là một phép gán biến).
-
Trình biên dịch C++ sẽ tự động đánh giá giá trị đó theo một quy tắc duy nhất:
-
Nếu kết quả bằng 0, điều kiện được xem là sai (false).
-
Trong tất cả các trường hợp còn lại (kết quả khác 0), điều kiện được xem là đúng (true).
-
Stdin:
Stdout:
2.2. Cấu trúc if-else:
-
Cấu trúc if-else cung cấp hai nhánh rẽ cho chương trình:
-
Nếu
biểu_thức_điều_kiệnĐÚNG, chương trình thực thi khối lệnh của if. -
Nếu
biểu_thức_điều_kiệnSAI, chương trình không bỏ qua như trước nữa, mà sẽ tự động nhảy vào thực thi khối lệnh của else.
-
Cú pháp của if-else:
if (biểu_thức_điều_kiện) {
// Khối lệnh 1: Được thực thi nếu điều kiện ĐÚNG (khác 0)
}
else {
// Khối lệnh 2: Được thực thi nếu điều kiện SAI (bằng 0)
}
- Ví dụ: Bài toán kiểm tra số chẵn, số lẻ
Loading...
Stdin:
Stdout:
Lưu ý: Trong cấu trúc rẽ nhánh, bạn có thể viết bao nhiêu lệnh if độc lập tùy thích. Tuy nhiên, một lệnh if chỉ có thể đi kèm tối đa một lệnh else tương ứng. Nếu bạn cố tình gắn nhiều else cho một if, trình biên dịch sẽ báo lỗi ngay lập tức vì nó không biết else dư thừa kia thuộc về điều kiện nào.
2.3. Cấu trúc khối lệnh if-else if-else:
Trong thực tế, một bài toán thường có nhiều hơn 2 nhánh rẽ. Thay vì viết nhiều lệnh if rời rạc khiến chương trình phải kiểm tra từng điều kiện một (rất tốn thời gian), C++ cung cấp cấu trúc if - else if - else để tạo ra một chuỗi các điều kiện liên kết với nhau.
Đặc điểm cốt lõi: Chương trình sẽ kiểm tra các điều kiện từ trên xuống dưới. Ngay khi có một điều kiện được thỏa mãn, khối lệnh tương ứng sẽ được thực thi, và chương trình sẽ lập tức thoát khỏi toàn bộ cấu trúc này mà không cần kiểm tra các điều kiện bên dưới nữa.
Cú pháp của if-else if-else:
if (biểu_thức_1) {
// Thực thi nếu biểu_thức_1 đúng
}
else if (biểu_thức_2) {
// Thực thi nếu biểu_thức_1 sai VÀ biểu_thức_2 đúng
}
else if (biểu_thức_3) {
// Thực thi nếu các biểu thức trên sai VÀ biểu_thức_3 đúng
}
else {
// (Tùy chọn) Thực thi nếu TẤT CẢ các điều kiện trên đều sai
}
-
Ví dụ:
Loading...Stdin:
Stdout:
Chú ý: Bạn có thể sử dụng bao nhiêu khối else if tùy thích để ghép nối các điều kiện.
- Khối else ở cuối cùng là không bắt buộc. Tuy nhiên, lập trình viên thường dùng nó để xét tất cả các trường hợp ngoại lệ (như việc người dùng nhập sai số ở ví dụ trên). Khối else này (nếu có) chỉ được xuất hiện 1 lần duy nhất ở cuối cấu trúc.
Bạn có thể copy-paste đoạn input sau đây vào stdin nhé:
3
2.4. Cấu trúc rẽ nhánh Switch-case
Cấu trúc switch-case là một giải pháp thay thế cho if-else if khi bạn cần so sánh sự bằng nhau của một biến với nhiều giá trị cụ thể.
Cấu trúc này sẽ lấy kết quả của biểu_thức, đem so sánh lần lượt với các giá trị ở từng case. Nếu khớp ở case nào, chương trình sẽ nhảy vào thực thi khối lệnh của case đó và chỉ dừng lại khi gặp từ khóa break.
Lưu ý cực kỳ quan trọng về kiểu dữ liệu: Trong C++, biểu thức bên trong switch() bắt buộc phải trả về kiểu số nguyên (int, long), ký tự (char), hoặc kiểu liệt kê (enum). Tuyệt đối không được dùng số thực (float, double) hay chuỗi (string).
Cú pháp của switch-case:
switch (biểu_thức) {
case gia_tri_1:
// Khối lệnh 1 sẽ chạy nếu biểu_thức == gia_tri_1
break; // Lệnh thoát khỏi switch
case gia_tri_2:
// Khối lệnh 2 sẽ chạy nếu biểu_thức == gia_tri_2
break;
...
default:
// Khối lệnh default (mặc định) sẽ chạy nếu không có case nào khớp.
// default không bắt buộc phải có.
}
Ví dụ 1:
Stdin:
Stdout:
-
Trong ví dụ nếu biểu thức có kết quả đúng các tên gọi của các case tương ứng thì sẽ chạy các khối lệnh trong case tương ứng đó.
-
Nếu giá trị của switch không tương ứng với bất kì case nào thì lệnh trong default sẽ được thực thi.
-
Ví dụ 2:
Stdin:
Stdout:
Hậu quả trong Ví dụ 2: Nếu người dùng nhập 1 (Đèn đỏ), màn hình sẽ in ra cả 2 dòng:
- Màu đỏ- dừng lại
- Màu vàng- chạy chậm lại!
Trình biên dịch sẽ chạy case 1, sau đó do không thấy break, nó tiếp tục "rơi tự do" xuống chạy luôn dòng lệnh của case 2 (dù lúc này biến là 1, không phải 2). Nó chỉ chịu dừng lại khi gặp break ở cuối case 2. Do đó, hãy luôn nhớ đặt break trừ khi bạn có ý đồ gộp chung nhiều case lại với nhau!
3. Cấu trúc lặp
Cấu trúc điều khiển thứ hai mà Ban học tập muốn mang đến cho các bạn là Cấu trúc lặp
Trong rất nhiều bài toán, chương trình cần làm đi làm lại một thao tác. Ví dụ:
- In các số từ 1 đến 100.
- Tính tổng từ 1 đến n.
- Nhập dữ liệu cho đến khi người dùng nhập đúng.
Nếu chỉ dùng các câu lệnh tuần tự, chương trình sẽ rất dài, khó sửa và dễ sai. Vì vậy, ta dùng cấu trúc lặp.
Trong C++, có 3 vòng lặp cơ bản:
for: phù hợp khi biết trước số lần lặp hoặc có biến đếm rõ ràng.while: phù hợp khi chưa biết trước số lần lặp, chỉ biết điều kiện dừng.do-while: giốngwhile, nhưng đảm bảo thân vòng lặp chạy ít nhất 1 lần.
3.1. Cấu trúc lặp for
for là vòng lặp có cấu trúc chặt chẽ nhất, thường dùng khi có biến đếm.
Cú pháp tổng quát:
for (khoi_tao; dieu_kien; cap_nhat) {
// than vong lap
}
Ý nghĩa từng phần:
khoi_tao: chạy đúng 1 lần trước khi vòng lặp bắt đầu (thường khai báo biến đếm).dieu_kien: được kiểm tra trước mỗi lần lặp.cap_nhat: chạy sau khi thân vòng lặp kết thúc, thường để tăng/giảm biến đếm.
Thứ tự chạy thực tế:
- Chạy
khoi_tao. - Kiểm tra
dieu_kien. - Nếu điều kiện đúng, chạy thân vòng lặp.
- Chạy
cap_nhat. - Quay về bước 2.
- Nếu điều kiện sai, thoát vòng lặp.
Flowchart vòng lặp for:
Ví dụ 1: Tính tổng từ 1 đến n
Stdin:
Stdout:
Thông qua ví dụ trên:
- khoi_tao: chính là bước khởi tạo biến
i(là biến đếm) của vòng lặpfor, biến này nhận giá trị từ 1 n (tức là n giá trị, tương ứng với n lần lặp) - dieu_kien: đây là điều kiện thực thi trước khi thực sự bắt đầu vòng lặp. Khối lệnh trong
forsẽ được thực thi (nếu dieu_kien trả vềtrue) và ngược lại, khối lệnh trongforsẽ không được thự thi (nếu dieu_kien trả vềfalse) - cap_nhat: chạy sau khi thân vòng lặp kết thúc, tăng biến đếm lên một đơn vị.
Dry-run nhanh với n = 4:
| Lần lặp | i | tong sau khi cộng |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 3 |
| 3 | 3 | 6 |
| 4 | 4 | 10 |
Khi i = 5, điều kiện i <= n sai, vòng lặp kết thúc.
Ví dụ 2: In các số chẵn từ 0 đến n
Stdin:
Stdout:
Copy-paste đoạn input sau đây vào ô stdin nhé:
10
Ở ví dụ này, phần cập nhật dùng i += 2 để nhảy theo bước 2.
Lưu ý quan trọng với for:
- Biến khai báo trong
for(ví dụint i) chỉ có phạm vi trong vòng lặp. - Có thể bỏ trống từng phần trong
for, nhưng phải giữ đủ 2 dấu;. for (;;)là vòng lặp vô hạn, chỉ dừng nếu cóbreak,return, hoặc chương trình kết thúc.
3.2. Cấu trúc lặp while
while là vòng lặp kiểm tra điều kiện trước khi vào thân vòng lặp.
Cú pháp tổng quát:
while (dieu_kien) {
// than vong lap
}
Nếu điều kiện sai ngay từ đầu, thân vòng lặp sẽ không chạy lần nào.
Khi nào nên dùng while?
- Khi không biết trước số lần lặp.
- Khi việc lặp phụ thuộc vào dữ liệu nhập trong lúc chạy.
Flowchart vòng lặp while:
Ví dụ: Nhập nhiều số, nhập 0 để dừng, tính tổng các số được nhập vào
Stdin:
Stdout:
Các bạn có thể chèn thông tin sau vào ô
stdinnhư input chox:
1
2
3
4
5
0
Mấu chốt của ví dụ này là phải đọc lại x bên trong vòng lặp, nếu không chương trình dễ bị lặp vô hạn.
3.3. Cấu trúc lặp do-while
do-while kiểm tra điều kiện sau khi chạy thân vòng lặp.
Cú pháp tổng quát:
do {
// than vong lap
} while (dieu_kien);
Lưu ý: Sau while (dieu_kien) bắt buộc có dấu ;.
Flowchart vòng lặp do-while:
Khi nào nên dùng do-while?
- Khi muốn thân vòng lặp chạy ít nhất một lần.
- Rất phù hợp cho menu chương trình.
Vòng lặp do-while luôn chạy thân vòng lặp ít nhất một lần
Ví dụ: Menu đơn giản lặp cho đến khi chọn 0 để thoát
Stdin:
Stdout:
Các bạn có thể copy-paste đoạn input sau vào ô stdin:
1
2
3
0
- Thông qua ví dụ trên, có thể thấy vòng lặp
do-whileđược thực hiện ít nhất một lần (in ra menu để điền lựa chọn). Nếu lựa chọn vẫn hợp lệ (thỏa biểu thức điều kiện của vòng lặp) thì vòng lặp tiếp tục được thực thi, nếu không thỏa mãn thì vòng lặp tự dừng lại.
3.4. Câu lệnh break và continue
Hai câu lệnh này thường dùng bên trong vòng lặp để điều khiển luồng chạy tinh gọn hơn.
break
- Dừng ngay vòng lặp gần nhất đang chứa nó.
- Chương trình tiếp tục ở câu lệnh ngay sau vòng lặp.
Ví dụ break: Tìm ước số đầu tiên lớn hơn 1 của n
Stdin:
Stdout:
continue
- Bỏ qua phần còn lại của lần lặp hiện tại.
- Chuyển sang lần lặp tiếp theo (nếu điều kiện vẫn còn đúng).
Ví dụ continue: Tính tổng các số lẻ từ 1 đến n
Stdin:
Stdout:
Copy-paste đoạn input sau đây vào ô stdin nhé:
10
3.5. So sánh nhanh for, while, do-while
| Vòng lặp | Kiểm tra điều kiện | Số lần chạy tối thiểu | Trường hợp dùng tốt nhất |
|---|---|---|---|
for | Trước thân vòng lặp | 0 | Biết trước số lần lặp, có biến đếm |
while | Trước thân vòng lặp | 0 | Chưa biết trước số lần lặp |
do-while | Sau thân vòng lặp | 1 | Cần chạy thân ít nhất một lần (menu, nhập lại dữ liệu) |
3.6. Lỗi thường gặp khi học vòng lặp
- Quên cập nhật biến đếm khiến vòng lặp chạy vô hạn.
- Viết sai điều kiện dừng (
<thay vì<=, hoặc ngược lại). - Quên dấu
;ở cuốido-while. - Lạm dụng
breaklàm luồng chương trình khó đọc. - Đặt quá nhiều logic phức tạp vào điều kiện vòng lặp.
Mẹo học tốt:
- Luôn thử bằng dữ liệu nhỏ (
n = 3,n = 5) để kiểm tra kết quả tay trước. - Nếu nghi ngờ sai, in tạm biến đếm trong mỗi lần lặp để quan sát.
- Đặt tên biến có nghĩa (
tong,dem,luaChon) thay vì chỉ dùnga,b,c.
Ví dụ minh họa:
Stdin:
Stdout:
Bài tập
- Xếp loại học sinh
Cho điểm kiểm tra Tin học gồm a, b, c, d với trọng số tương ứng: a và b hệ số 1, c hệ số 2, d hệ số 3.
- Tính điểm trung bình có trọng số.
- In kết quả:
>= 8: Giỏi>= 6.5và< 8: Khá>= 5và< 6.5: Trung Bình< 5: Yếu
- Tìm số lớn nhất và nhỏ nhất
Nhập 4 số nguyên a, b, c, d, in ra số lớn nhất và số nhỏ nhất.
- Chúc mừng năm mới
Nhập giờ h (0 <= h < 24) và phút p (0 <= p < 60) của thời điểm hiện tại.
- Tính số phút còn lại đến
00:00ngày hôm sau. - Gợi ý: Số phút đã trôi qua trong ngày là
h * 60 + p.
- Kiểm tra các chữ số không giảm
Nhập số nguyên dương n >= 0.
- In
Cac so khong giamnếu các chữ số từ trái sang phải không giảm. - Ngược lại in
Cac so tang giam.
Ví dụ:
125778->Cac so khong giam125478->Cac so tang giam
- Phân loại deficient, perfect, abundant
Cho số nguyên dương x, gọi s là tổng các ước dương thực sự của x (không tính chính x).
s < x: deficients = x: perfects > x: abundant
Nhập hai số nguyên dương m, n (m <= n) và in phân loại của từng số trong đoạn [m, n].
Ví dụ:
8là deficient vì1 + 2 + 4 < 86là perfect vì1 + 2 + 3 = 612là abundant vì1 + 2 + 3 + 4 + 6 > 12
- Tìm số Harshad trong đoạn [m, n]
Số Harshad là số chia hết cho tổng các chữ số của nó.
- Nhập
m,n, in các số Harshad trong đoạn. - Nếu không có, in
NotFound.
Ví dụ: 18 là Harshad vì 18 % (1 + 8) = 0.
- Tìm số Automorphic trong đoạn [m, n]
Số Automorphic là số mà bình phương của nó kết thúc bằng chính nó.
- Nhập
m,n, in các số Automorphic trong đoạn. - Nếu không có, in
NotFound.
Ví dụ: 5^2 = 25, 6^2 = 36, 25^2 = 625, 76^2 = 5776.