Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 6.514 phút đọc

Điều khiển vòng lặp

Sau bài này bạn sẽ làm được

  • Dùng break và continue đúng ngữ cảnh vòng lặp
  • Thoát nhiều tầng vòng lặp bằng ba cách khác nhau
  • Giải thích vì sao continue trong while dễ gây vòng lặp vô hạn
  • Biết khi nào nên đưa điều kiện dừng vào phần điều kiện thay vì dùng break

Bài 5.6 và 5.7 đã giới thiệu breakcontinue khi bạn chưa học vòng lặp. Giờ đã có đủ nền, bài này gom lại đầy đủ bốn công cụ điều khiển vòng lặp, kèm ba cách thoát nhiều tầng và một lời cảnh báo về việc lạm dụng.

#Bốn công cụ

LệnhTác dụngPhạm vi ảnh hưởng
breakThoát hẳn khỏi vòng lặpMột tầng gần nhất
continueBỏ qua phần còn lại, sang lượt tiếp theoMột tầng gần nhất
goto nhanNhảy tới nhãn bất kỳ trong cùng hàmBao nhiêu tầng cũng được
returnThoát khỏi cả hàmToàn bộ, kể cả mọi tầng vòng lặp

#break trong vòng lặp

/* Tìm phần tử, dừng ngay khi thấy */
long vi_tri = -1;

for (size_t i = 0; i < n; ++i) {
    if (a[i] == muc_tieu) {
        vi_tri = (long)i;
        break;
    }
}

Lợi ích không chỉ là gọn. Với mảng một triệu phần tử mà phần tử cần tìm nằm ở vị trí thứ mười, có break thì dừng sau 10 lượt, không có thì chạy đủ một triệu lượt.

#continue và bẫy của while

continue nhảy tới cuối thân vòng lặp, bỏ qua mọi câu lệnh còn lại. Nhưng chuyện gì xảy ra tiếp theo thì khác nhau giữa forwhile, và đó chính là cạm bẫy.

Vòng lặpSau continue thì làm gìRủi ro
forChạy phần cập nhật rồi kiểm tra điều kiệnKhông có, biến chạy vẫn tiến
whileKiểm tra điều kiện ngayCao, nếu phần tăng biến nằm sau continue
do whileKiểm tra điều kiện ở cuối ngayCao, tương tự while
Không nên
int i = 0;

while (i < 10) {
    if (i % 2 == 0) {
        continue;       /* nhảy thẳng về điều kiện, i KHÔNG tăng */
    }

    printf("%d ", i);
    i++;
}
/* Vòng lặp vô hạn ngay từ i bằng 0. */
Nên
for (int i = 0; i < 10; ++i) {
    if (i % 2 == 0) {
        continue;       /* an toàn, phần ++i vẫn chạy */
    }

    printf("%d ", i);
}

Ứng dụng chính là lọc dữ liệu

char dong[256];

while (fgets(dong, sizeof dong, f) != NULL) {
    dong[strcspn(dong, "\n")] = '\0';

    if (dong[0] == '\0') {
        continue;              /* bỏ qua dòng trống */
    }

    if (dong[0] == '#') {
        continue;              /* bỏ qua dòng chú thích */
    }

    /* Phần xử lý chính không bị thụt lề thêm tầng nào. */
    phan_tich(dong);
}

Ở đây while an toàn vì không có biến đếm nào cần tăng. Việc tiến tới dòng tiếp theo do fgets ở phần điều kiện lo, mà phần điều kiện thì luôn được chạy.

#Thoát nhiều tầng

C không có break kèm nhãn như Java hay Go. Có ba cách thay thế, và cách thứ ba thường tốt nhất.

Cách 1: dùng cờ

int xong = 0;

for (int i = 0; i < n && !xong; ++i) {
    for (int j = 0; j < m; ++j) {
        if (a[i][j] == muc_tieu) {
            xong = 1;
            break;
        }
    }
}

Dễ hiểu, nhưng phải kiểm tra cờ ở điều kiện của mọi tầng bên ngoài. Với ba tầng trở lên thì khá rườm rà.

Cách 2: dùng goto

for (int i = 0; i < n; ++i) {
    for (int j = 0; j < m; ++j) {
        if (a[i][j] == muc_tieu) {
            goto tim_thay;
        }
    }
}

printf("Không tìm thấy\n");
goto ket_thuc;

tim_thay:
printf("Tìm thấy\n");

ket_thuc: ;

Cách 3: tách thành hàm riêng

int tim_trong_ma_tran(int a[][M], int n, int muc_tieu,
                      int *hang, int *cot)
{
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < M; ++j) {
            if (a[i][j] == muc_tieu) {
                *hang = i;
                *cot  = j;

                return 1;      /* return thoát khỏi MỌI tầng cùng lúc */
            }
        }
    }

    return 0;
}

#Khi nào không nên dùng

breakcontinue rất tiện, nhưng dùng nhiều làm vòng lặp khó theo dõi, vì người đọc phải tìm hết mọi lối thoát mới hiểu được vòng lặp kết thúc khi nào.

Điều kiện dừng bị giấu trong thân
size_t i = 0;

while (1) {
    if (i >= n) {
        break;
    }

    if (a[i] == x) {
        break;
    }

    ++i;
}
Điều kiện dừng nằm ngay ở đầu
size_t i = 0;

while (i < n && a[i] != x) {
    ++i;
}

if (i < n) {
    printf("Tìm thấy tại %zu\n", i);
}

Trường hợp break là lựa chọn đúng

while (1) {
    char dong[256];

    if (fgets(dong, sizeof dong, stdin) == NULL) {
        break;                        /* hết dữ liệu, chỉ biết sau khi đọc */
    }

    dong[strcspn(dong, "\n")] = '\0';

    if (strcmp(dong, "thoat") == 0) {
        break;                        /* người dùng yêu cầu dừng */
    }

    xu_ly(dong);
}

Ở đây hai điều kiện dừng đều chỉ xác định được sau khi đã đọc xong một dòng, nên không thể đưa lên phần điều kiện. Dùng break là hợp lý và rõ ràng.

Tự làm thử

  1. Tìm số nguyên tố đầu tiên lớn hơn một triệu, dùng break để dừng ngay khi thấy. Đếm số lượt lặp thực tế.
  2. Tái hiện vòng lặp vô hạn bằng continue trong while, rồi sửa bằng hai cách khác nhau.
  3. Tìm một giá trị trong ma trận hai chiều bằng cả ba cách thoát nhiều tầng, so sánh độ dài và độ dễ đọc của ba bản.
  4. Đọc file, đếm số dòng có nội dung, bỏ qua dòng trống và dòng bắt đầu bằng dấu thăng.
  5. Viết lại một vòng lặp có break thành vòng lặp có điều kiện dừng nằm trong phần điều kiện. Chỉ ra trường hợp nào không viết lại được.
  6. Đặt break trong switch nằm trong while, quan sát vòng lặp vô hạn, rồi sửa bằng cờ.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Bốn công cụ điều khiển vòng lặp là break, continue, goto và return.
  • breakcontinue chỉ tác động một tầng gần nhất.
  • Trong for, continue vẫn chạy phần cập nhật. Trong while thì không, nên dễ gây vòng lặp vô hạn.
  • break trong switch nằm trong vòng lặp chỉ thoát switch.
  • Thoát nhiều tầng bằng cờ, bằng goto tiến xuống, hoặc tách thành hàm rồi return.
  • Ưu tiên đưa điều kiện dừng vào phần điều kiện của vòng lặp thay vì rải break trong thân.