Menu

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

Cách vòng lặp while trong C lặp lại cho đến khi điều kiện thay đổi: quy tắc kiểm tra điều kiện trước, vòng lặp dùng giá trị canh (sentinel), đọc đầu vào đến EOF, while(1), và chuyển đổi qua lại giữa for và while.

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.

Lặp lại khi không biết trước số lần

Vòng lặp for là công cụ phù hợp khi bạn biết, hoặc tính được, cần lặp bao nhiêu lần. Nhưng rất nhiều việc không đi kèm số lần lặp. Đọc từng dòng của một tệp cho đến hết. Hỏi mật khẩu cho đến khi nhập đúng. Chia đôi một số cho đến khi còn 1. Với những việc này, câu hỏi không phải là "bao nhiêu lần" mà là "cho đến khi nào", và đó chính xác là điều while diễn đạt.

Không ai tính trước con số "bảy". Vòng lặp (loop) chỉ đơn giản tiếp tục chạy khi điều kiện còn đúng.

Cú pháp và quy tắc kiểm tra điều kiện trước

while (condition) {
    // body - runs while the condition is nonzero
}

Điều kiện được kiểm tra trước mỗi lần lặp, kể cả lần đầu tiên. Điều này dẫn tới ba hệ quả:

  • Một vòng lặp có điều kiện sai ngay từ đầu sẽ chạy không lần nào. while (0) { ... } không bao giờ thực thi thân vòng lặp.
  • Thứ mà điều kiện kiểm tra phải được khởi tạo trước vòng lặp, nếu không bạn đang kiểm tra một giá trị rác.
  • Phải có thứ gì đó trong thân vòng lặp cuối cùng làm điều kiện sai đi, nếu không vòng lặp sẽ không bao giờ kết thúc.

Giống như if, dấu ngoặc tròn là bắt buộc và không có dấu chấm phẩy sau chúng. Dấu chấm phẩy thừa đó chính là phiên bản vòng lặp của cùng một bug:

/* BUG: the empty statement is the body - this spins forever */
while (i < 10);
{
    printf("%d\n", i);
    i++;
}

i không bao giờ được tăng, nên điều kiện không bao giờ thay đổi. gcc -Wall sẽ cảnh báo về thân vòng lặp rỗng.

Quy tắc đúng sai của C áp dụng ở đây y hệt như trong if/else: vòng lặp tiếp tục khi biểu thức khác 0, và dừng khi nó bằng 0.

Khuôn mẫu chuẩn: khởi tạo, kiểm tra, cập nhật

Gần như mọi vòng lặp while đúng đều có ba phần nằm ở ba chỗ khác nhau:

Đó cũng chính là ba phần nằm trong dòng đầu của for, chỉ là không gom lại trên một dòng. Và đó là câu trả lời thật lòng cho câu hỏi "for hay while?": nếu cả ba phần đều xoay quanh một biến đếm, for đặt chúng ở chỗ người đọc có thể kiểm tra cùng lúc. Nếu phần cập nhật nằm rải rác trong một thân vòng lặp dài, hoặc điều kiện không liên quan tới việc đếm, while là cách viết rõ ràng hơn.

Quên phần 3 là bug kinh điển nhất của vòng lặp while. Nếu một vòng lặp bị treo, việc đầu tiên cần kiểm tra là thân vòng lặp có thật sự thay đổi thứ mà điều kiện đọc hay không.

Vòng lặp sentinel

Sentinel (giá trị canh) là một giá trị đặc biệt mang nghĩa "dừng lại". Tính tổng các số cho đến khi người dùng nhập 0:

Số 99 sau 0 không bao giờ được đọc tới, vì sentinel đã kết thúc vòng lặp. Để ý rằng số lượng số không hề được biết trước, và đó chính là lý do đây là một vòng while.

Chuỗi trong C cũng hoạt động theo cách đó: chúng kết thúc bằng sentinel '\0', nên duyệt qua một chuỗi là ví dụ kinh điển của while:

Đọc đầu vào đến EOF

Vòng while thường gặp nhất trong thực tế là đọc đầu vào cho đến khi hết. Quy tắc là kiểm tra giá trị trả về của hàm nhập, không phải giá trị mà nó đã lưu.

getchar trả về ký tự tiếp theo, hoặc hằng số EOF khi hết đầu vào:

Có hai chi tiết quan trọng. c là int vì EOF là một giá trị âm không vừa trong char trên nhiều hệ thống: khai báo c là char có thể khiến phép so sánh không bao giờ đúng. Và phép gán nằm ngay trong điều kiện: (c = getchar()) != EOF đọc một ký tự, lưu lại và so sánh trong một bước. Cặp ngoặc bên trong là bắt buộc, vì != có độ ưu tiên cao hơn =; thiếu chúng, bạn sẽ lưu kết quả của phép so sánh vào c. Đây là một trong số ít chỗ mà phép gán trong điều kiện là cách viết chuẩn chứ không phải bug.

Khuôn mẫu này cũng dùng được với scanf, hàm trả về số mục đã chuyển đổi thành công:

int n;
while (scanf("%d", &n) == 1) {
    /* n holds a valid number */
}

Vòng lặp kết thúc khi hết dữ liệu và khi gặp đầu vào đầu tiên không phải số, thường đó chính là điều bạn muốn.

Điều bạn không nên viết là while (!feof(f)). feof cho biết một lần đọc đã thất bại hay chưa, nên khi bạn đang ở mục cuối cùng thì nó vẫn là sai, thân vòng lặp chạy thêm một lần, lần đọc đó thất bại và chương trình xử lý dữ liệu cũ. Hãy kiểm tra chính lần đọc.

while (1) và break

Đôi khi điều kiện thoát chỉ biết được ở giữa thân vòng lặp: sau khi đọc, nhưng trước khi xử lý. Cách viết chuẩn là một vòng lặp cố ý chạy vô hạn kèm một lối thoát rõ ràng:

while (1) luôn đúng, nên cách duy nhất để thoát là break (hoặc return, hoặc exit). Bạn cũng sẽ thấy for (;;) được dùng cho cùng mục đích; cả hai biên dịch ra cùng một thứ, và codebase chọn cách nào là chuyện phong cách.

Dùng khuôn mẫu này khi phép kiểm tra thoát thật sự thuộc về giữa thân vòng lặp. Khi phép kiểm tra nằm tự nhiên ở đầu, hãy đặt nó vào điều kiện, nơi người đọc nhìn thấy được: một while (1) mà break duy nhất là câu lệnh đầu tiên thì chỉ là một điều kiện đội lốt.

Chuyển đổi giữa for và while

Mọi vòng lặp for đều viết được thành vòng lặp while và ngược lại. Cách chuyển rất máy móc:

/* for */                          /* equivalent while */
for (init; cond; update) {         init;
    body;                          while (cond) {
}                                      body;
                                       update;
                                   }

Nên hai đoạn này in ra cùng một kết quả:

Chỗ duy nhất mà sự tương đương này bị hở là continue. Trong vòng for, continue nhảy tới phần cập nhật trong dòng đầu, nên biến đếm vẫn tăng. Trong phiên bản while, continue nhảy thẳng về điều kiện và bỏ qua i++ ở cuối thân vòng lặp, lập tức thành vòng lặp vô hạn:

/* BUG: continue skips i++, so the loop spins forever on the first even number */
int i = 0;
while (i < 10) {
    if (i % 2 == 0) {
        continue;
    }
    printf("%d ", i);
    i++;
}

Nếu một vòng while dùng continue, hãy chắc rằng phần cập nhật diễn ra trước nó, hoặc dùng for, nơi dòng đầu đảm bảo điều đó.

Lỗi thường gặp

  • Không có gì cập nhật điều kiện. Thân vòng lặp phải thay đổi thứ mà điều kiện đọc. Nếu chương trình bị treo, hãy xem chỗ này trước.
  • Dấu chấm phẩy sau dòng đầu. while (cond); biến câu lệnh rỗng thành thân vòng lặp.
  • Kiểm tra một biến chưa từng được khởi tạo. Điều kiện chạy trước thân vòng lặp, nên giá trị nó đọc phải có sẵn từ trước.
  • Dùng char thay vì int cho getchar. EOF không vừa trong char; khi đó phép so sánh có thể không bao giờ đúng, hoặc một byte hợp lệ bị nhầm thành EOF.
  • while (!feof(f)). Lặp thừa một lần. Hãy kiểm tra lần đọc.
  • Nghĩ rằng thân vòng lặp chạy ít nhất một lần. Không phải vậy. Khi bạn cần chắc chắn chạy một lần, như in menu trước khi kiểm tra lựa chọn, đó là việc của vòng lặp do-while.

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

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

while (condition) { body }. Điều kiện được kiểm tra trước mỗi lần lặp, nên thân vòng lặp chỉ chạy khi điều kiện khác 0, và không chạy lần nào nếu ngay từ đầu điều kiện đã sai. Phải có thứ gì đó trong thân vòng lặp làm điều kiện thay đổi, nếu không vòng lặp sẽ không bao giờ kết thúc.

for và while trong C khác nhau thế nào?

Hai loại có sức mạnh như nhau: mọi vòng for đều viết lại được thành while. Dùng for khi biến đếm, giới hạn và bước nhảy đi cùng nhau trong một dòng đầu, và dùng while khi điều kiện dừng không phải là số lần đếm: đọc đến hết tệp, thử lại đến khi thành công, hoặc lặp đến khi một cờ đổi trạng thái.

Làm sao đọc đầu vào đến EOF trong C?

Kiểm tra giá trị trả về của hàm nhập, không phải giá trị nó đọc được: while (scanf("%d", &n) == 1) { ... } dừng khi hết dữ liệu hoặc gặp đầu vào đầu tiên không phải số, còn while ((c = getchar()) != EOF) { ... } đọc từng ký tự. Đừng bao giờ lặp bằng while (!feof(f)): vòng lặp sẽ chạy thừa một lần sau khi hết dữ liệu.

Vì sao vòng lặp while trong C bị lặp vô hạn?

Thường là vì không có gì trong thân vòng lặp thay đổi giá trị mà điều kiện kiểm tra, hoặc vì một dấu chấm phẩy thừa sau dòng đầu (while (i < 10);) đã biến câu lệnh rỗng thành thân vòng lặp. Cả hai đều biên dịch không lỗi, nên hãy kiểm tra xem thân vòng lặp có thật sự cập nhật biến trong điều kiện không.

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