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

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 C++\texttt{C++}, 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ĩaC++
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ìnhgoto, 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ínhfunction 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ặpwhile, do-while, for, range-based for
Lệnh dừng (Halts)Dừng chương trìnhstd::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ụ:
Loading...

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).

Loading...

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ện SAI, 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:

Loading...

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:

Loading...

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ống while, 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ế:

  1. Chạy khoi_tao.
  2. Kiểm tra dieu_kien.
  3. Nếu điều kiện đúng, chạy thân vòng lặp.
  4. Chạy cap_nhat.
  5. Quay về bước 2.
  6. 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

Loading...

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ặp for, biến này nhận giá trị từ 1 →\rightarrow 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 for sẽ được thực thi (nếu dieu_kien trả về true) và ngược lại, khối lệnh trong for sẽ 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ặpitong sau khi cộng
111
223
336
4410

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

Loading...

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

Loading...

Stdin:

Stdout:

Các bạn có thể chèn thông tin sau vào ô stdin như input cho x:
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

Loading...

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

Loading...

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

Loading...

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ặpKiểm tra điều kiệnSố lần chạy tối thiểuTrường hợp dùng tốt nhất
forTrước thân vòng lặp0Biết trước số lần lặp, có biến đếm
whileTrước thân vòng lặp0Chưa biết trước số lần lặp
do-whileSau thân vòng lặp1Cầ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​

  1. Quên cập nhật biến đếm khiến vòng lặp chạy vô hạn.
  2. Viết sai điều kiện dừng (< thay vì <=, hoặc ngược lại).
  3. Quên dấu ; ở cuối do-while.
  4. Lạm dụng break làm luồng chương trình khó đọc.
  5. Đặ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ùng a, b, c.

Ví dụ minh họa:

Loading...

Stdin:

Stdout:


Bài tập

  1. 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.5 và < 8: Khá
    • >= 5 và < 6.5: Trung Bình
    • < 5: Yếu
  1. 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.

  1. 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:00 ngày hôm sau.
  • Gợi ý: Số phút đã trôi qua trong ngày là h * 60 + p.
  1. Kiểm tra các chữ số không giảm

Nhập số nguyên dương n >= 0.

  • In Cac so khong giam nế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 giam
  • 125478 -> Cac so tang giam
  1. 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: deficient
  • s = x: perfect
  • s > 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ụ:

  • 8 là deficient vì 1 + 2 + 4 < 8
  • 6 là perfect vì 1 + 2 + 3 = 6
  • 12 là abundant vì 1 + 2 + 3 + 4 + 6 > 12
  1. 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.

  1. 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.


Tài liệu tham khảo

  1. UIT - Cấu trúc rẽ nhánh
  2. UIT - Vòng lặp