Menu

Vòng lặp for trong C: cú pháp, ví dụ và lỗi thường gặp

Cách lặp lại code bằng vòng lặp for trong C: phần đầu ba thành phần, đếm lên và đếm xuống, duyệt mảng với mẹo sizeof, vòng lặp lồng nhau, vòng lặp vô hạn, và các bug lệch một đơn vị và unsigned mà ai cũng từng mắc.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Vì sao cần vòng lặp for

if và switch chọn một nhánh và chạy nó một lần. Chương trình thực tế cần làm việc lặp đi lặp lại: in mọi điểm số, cộng một danh sách số, vẽ mười hàng của một lưới, thử từng ký tự trong một chuỗi. Vòng lặp (loop) for là công cụ chủ lực của C để lặp lại code một số lần đã biết, với một biến đếm do bạn kiểm soát.

Mọi thứ một vòng lặp for cần đều nằm gọn trong phần đầu, nên "bao nhiêu lần, và lặp như thế nào" hiện rõ trên một dòng.

Phần đầu ba thành phần

Phần đầu của for có ba thành phần ngăn cách bằng dấu chấm phẩy: khởi tạo, điều kiện và cập nhật.

for (initializer; condition; update) {
    // body - runs while the condition is nonzero
}

Chúng chạy theo một thứ tự cụ thể, và nắm được thứ tự này sẽ giải thích mọi vòng lặp for bạn từng đọc:

  1. Phần khởi tạo chạy một lần, trước mọi thứ khác.
  2. Điều kiện được kiểm tra. Nếu bằng 0, vòng lặp kết thúc ngay.
  3. Thân vòng lặp chạy.
  4. Phần cập nhật chạy.
  5. Quay lại bước 2.

Lần theo vòng lặp ở trên với danh sách đó: int i = 0 chạy một lần. 0 < 5 đúng, nên thân in ra i = 0, rồi i++ làm i bằng 1. Điều kiện được kiểm tra lại, và cứ thế tiếp tục. Khi i đạt 5, phép kiểm tra thất bại, thân bị bỏ qua và done được in ra. Thân đã chạy đúng năm lần, với i lần lượt nhận giá trị từ 0 đến 4.

Có hai hệ quả trực tiếp. Điều kiện được kiểm tra trước lần chạy đầu tiên, nên một vòng lặp có điều kiện sai ngay từ đầu sẽ chạy 0 lần: for (int i = 10; i < 5; i++) không bao giờ vào thân. Và phần cập nhật chạy sau thân, nên trong suốt thân vòng lặp i vẫn giữ giá trị cũ.

Khai báo biến đếm ngay trong phần đầu (int i = 0) hợp lệ từ C99 và là lựa chọn mặc định đúng: khi đó i chỉ tồn tại bên trong vòng lặp, nên không thể trùng với thứ gì phía sau, và mỗi vòng lặp trong một hàm có thể thoải mái dùng lại tên đó.

Đếm lên, đếm xuống và đếm theo bước

Phần cập nhật không chỉ giới hạn ở i++. Bạn có thể đếm xuống, nhảy theo bước bất kỳ hoặc nhân đôi sau mỗi lần lặp:

Hãy cho điều kiện khớp với phần cập nhật. Đếm lên đi với < hoặc <=; đếm xuống đi với > hoặc >=. Ghép sai cặp này là cách bạn viết ra một vòng lặp không bao giờ kết thúc.

Để ý vòng lặp thứ hai dùng <= 10 vì 10 là giá trị ta muốn bao gồm, còn ví dụ đầu tiên dùng < 5 vì 5 thì không. Lựa chọn đó là nơi bug lệch một đơn vị ra đời, nên hãy chọn có ý thức: i < n chạy n lần khi bắt đầu từ 0; i <= n chạy n + 1 lần.

Duyệt qua một mảng

Cách dùng phổ biến nhất của vòng lặp đếm là duyệt mảng theo chỉ số. Biến đếm kiêm luôn vai trò vị trí bạn đọc.

Có hai điều cần rút ra từ ví dụ này.

Mẹo sizeof. sizeof(scores) là kích thước cả mảng tính bằng byte, còn sizeof(scores[0]) là kích thước một phần tử, nên thương của chúng là số phần tử. Nó tự điều chỉnh khi bạn thêm phần tử, điều mà một số 5 viết cứng không làm được. Điểm cần lưu ý, và đây là điểm lớn, là nó chỉ đúng ở nơi bản thân mảng nằm trong phạm vi. Truyền mảng vào một hàm thì nó suy biến thành con trỏ, nên sizeof ở đó cho ra kích thước của một con trỏ. Bên trong hàm, luôn nhận độ dài qua một tham số riêng.

Điều kiện là i < n, không bao giờ là i <= n. Một mảng năm phần tử có chỉ số hợp lệ từ 0 đến 4. Đọc scores[5] là undefined behavior: nó có thể in ra giá trị rác, gây sập, hoặc trông như vẫn chạy trong khi âm thầm làm hỏng thứ khác. C hoàn toàn không kiểm tra giới hạn, nên trách nhiệm này nằm trọn ở bạn.

Duyệt mảng ngược lại cũng là ý tưởng đó theo chiều ngược:

Bắt đầu ở n - 1 (chỉ số hợp lệ cuối cùng), dừng ở 0 và bao gồm cả 0, vì thế dùng >= chứ không phải >. Ở đây hãy giữ i là một int có dấu; phần tiếp theo giải thích lý do.

Vòng lặp lồng nhau

Đặt một for bên trong một for khác để làm việc với lưới, bảng hoặc mọi cặp phần tử. Vòng lặp trong chạy trọn vẹn cho mỗi bước của vòng lặp ngoài.

Vòng lặp ngoài cố định một row; vòng lặp trong quét mọi col của hàng đó; lệnh printf("\n") sau vòng lặp trong kết thúc dòng. Chuyển lệnh xuống dòng đó vào trong vòng lặp trong thì cả bảng sẽ thành một cột, một thử nghiệm đáng làm vì nó khiến cấu trúc lồng nhau trở nên cụ thể.

Hãy đặt tên khác nhau cho các biến đếm. row/col hoặc i/j đều ổn; dùng lại i cho cả hai sẽ che khuất biến đếm ngoài và cho ra kết quả khó hiểu. Và hãy để ý chi phí: một vòng lặp n lần bên trong một vòng lặp n lần chạy thân n * n lần, nên hai vòng lặp 1.000 lần lồng nhau là một triệu lần lặp.

Đây là một vòng lặp lồng nhau làm việc thật: một tam giác ngôi sao, trong đó giới hạn của vòng lặp trong phụ thuộc vào biến đếm ngoài:

break và continue

Hai từ khóa thay đổi luồng chạy giữa chừng. break thoát vòng lặp ngay lập tức; continue bỏ qua phần còn lại của lần lặp hiện tại và nhảy tới phần cập nhật.

Vòng lặp đầu dừng ngay khi tìm thấy 7 và không bao giờ kiểm tra 93 giá trị còn lại. Vòng lặp thứ hai dùng continue để bỏ qua lệnh in với số chẵn; lệnh i++ ở phần đầu vẫn chạy, nên vòng lặp vẫn tiến lên. Còn nhiều điều nữa về cả hai trong bài break và continue, kể cả cách thoát khỏi một vòng lặp lồng nhau, điều mà một break đơn lẻ không làm được.

Vòng lặp vô hạn và các thành phần bỏ trống

Cả ba thành phần của phần đầu đều không bắt buộc. Bỏ điều kiện đi thì nó được coi là luôn đúng:

for (;;) {
    /* runs forever - exit with break or return */
}

for (;;) là cách viết vòng lặp vô hạn quen thuộc trong C, và các dấu chấm phẩy vẫn bắt buộc. Đây không phải lỗi miễn là bên trong có thứ gì đó có thể break, return hoặc exit: vòng lặp sự kiện và vòng lặp "hỏi mãi cho đến khi đầu vào hợp lệ" được viết đúng theo cách này.

Các thành phần khác cũng có thể bỏ. Nếu biến đếm đã tồn tại, bỏ phần khởi tạo; nếu thân vòng lặp tự tăng biến đếm, bỏ phần cập nhật:

int i = 0;
for (; i < n; ) {
    /* ... */
    i += step;
}

Cách này hợp lệ nhưng khó đọc hơn vòng lặp while mà thực chất nó là. Hãy chọn for khi biến đếm, giới hạn và bước nhảy thuộc về nhau trong phần đầu, và chọn vòng lặp while khi không phải vậy.

Toán tử dấu phẩy trong phần đầu

Phần khởi tạo và phần cập nhật đều có thể chứa nhiều biểu thức ngăn cách bằng dấu phẩy, đó là cách chạy hai biến đếm cùng lúc:

int i = 0, j = len - 1 khai báo cả hai biến đếm, và i++, j-- tăng giảm cả hai. Đây là toán tử dấu phẩy, tính các toán hạng theo thứ tự từ trước ra sau. Hãy dùng nó cho các biến đếm thực sự song song; nhồi những việc không liên quan vào phần đầu chỉ khiến chúng bị che khuất.

Những cái bẫy thường gặp

Một vài cái bẫy chiếm phần lớn các bug vòng lặp for trong C.

Lệch một đơn vị. i <= n trên một mảng đánh chỉ số từ 0 sẽ đọc thêm một phần tử vượt quá cuối mảng. Dùng i < n cho "n lần bắt đầu từ 0".

Dấu chấm phẩy sau phần đầu. Đoạn này biên dịch được và gần như không bao giờ là điều bạn muốn:

/* BUG: the loop body is the empty statement; the printf runs once */
for (int i = 0; i < 5; i++);
{
    printf("%d\n", i);
}

Dấu ; trở thành thân vòng lặp, nên vòng lặp quay năm lần mà không làm gì, còn khối ngoặc nhọn bên dưới chạy một lần như một khối lệnh bình thường. gcc -Wall có cảnh báo về điều này.

Biến đếm unsigned đếm xuống. Đây là một vòng lặp vô hạn:

/* BUG: an unsigned value is never < 0 */
for (size_t i = n - 1; i >= 0; i--) {
    process(a[i]);
}

i >= 0 luôn đúng với kiểu không dấu. Khi i đạt 0, i-- quay vòng thành một số dương khổng lồ và vòng lặp tiếp tục chạy vào vùng nhớ không thuộc về nó. Hãy dùng int có dấu cho việc đếm xuống, hoặc viết điều kiện thành for (size_t i = n; i-- > 0; ), cách này giảm và kiểm tra trong một bước và dừng đúng ở 0.

Thay đổi biến đếm ở cả hai nơi. Thay đổi i cả trong thân lẫn trong phần đầu khiến số lần lặp khó đoán trước. Hãy chọn một nơi.

Biến đếm số thực. for (double x = 0.0; x != 1.0; x += 0.1) có thể không bao giờ kết thúc, vì 0.1 không có biểu diễn nhị phân chính xác và tổng sẽ bước qua 1.0 mà không chạm đúng nó. Hãy lặp bằng một số đếm nguyên và tính giá trị bên trong:

for (int i = 0; i <= 10; i++) {
    double x = i / 10.0;
    /* ... */
}

Thay đổi độ dài mảng trong khi đang duyệt nó. Điều kiện i < n đọc lại n ở mỗi lần lặp, nên nếu thân vòng lặp làm tập hợp nhỏ đi, hãy điều chỉnh chỉ số một cách có chủ đích thay vì để hai thứ lệch nhau.

Câu hỏi thường gặp

Viết vòng lặp for trong C như thế nào?

Đặt ba thành phần vào phần đầu, ngăn cách bằng dấu chấm phẩy: một biểu thức khởi tạo, một điều kiện và một lệnh cập nhật: for (int i = 0; i < 5; i++) { printf("%d\n", i); }. Vòng lặp đó chạy thân năm lần với i lần lượt nhận giá trị từ 0 đến 4, và dừng ngay khi điều kiện sai.

Duyệt mảng trong C như thế nào?

Đếm chỉ số từ 0 đến trước độ dài (không bao gồm độ dài): for (int i = 0; i < n; i++) { sum += a[i]; }. Với mảng khai báo trong cùng hàm, bạn có thể tính độ dài bằng sizeof(a) / sizeof(a[0]); khi mảng đã được truyền vào một hàm thì mẹo đó không còn đúng, nên hãy truyền độ dài như một tham số.

Vì sao vòng lặp for trong C của tôi chạy thừa một lần?

Đó là bug lệch một đơn vị (off-by-one). Với mảng có kích thước n đánh chỉ số từ 0, các chỉ số hợp lệ là từ 0 đến n - 1, nên i <= n chạy thừa một lần lặp và đọc vượt quá cuối mảng, tức undefined behavior. Mặc định an toàn là i < n.

Có khai báo biến đếm bên trong vòng lặp for của C được không?

Được, kể từ C99: for (int i = 0; i < n; i++). Khi đó biến chỉ tồn tại bên trong vòng lặp, đúng như bạn muốn. Biên dịch với gcc -std=c99 hoặc mới hơn (GCC và clang hiện đại mặc định dùng C17, nên chạy được ngay); kiểu C89 cũ khai báo i trước vòng lặp.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu