Khi không có biến đếm
Vòng lặp for được xây dựng quanh một biến đếm bạn khởi tạo từ đầu. Nhưng rất nhiều vòng lặp không có số lần lặp rõ ràng: đọc số liên tục đến khi hết dữ liệu nhập, hỏi mật khẩu đến khi nhập đúng, chia đôi một giá trị đến khi nó bằng 1. Với những trường hợp đó, vòng lặp (loop) while là lựa chọn tự nhiên: nó chỉ đơn giản lặp lại chừng nào điều kiện còn đúng.
Vòng lặp while kiểm tra điều kiện trước mỗi lần lặp, kể cả lần đầu tiên. Nếu ngay từ đầu điều kiện đã sai, thân vòng lặp sẽ không chạy lần nào.
Điều kiện count > 0 được kiểm tra trước; nếu đúng thì thân vòng lặp chạy, sau đó quay lại kiểm tra tiếp. Dòng count-- chính là thứ cuối cùng làm điều kiện thành sai. Bỏ dòng đó đi, vòng lặp sẽ chạy mãi không dừng.
Cấu tạo của vòng lặp while
Hãy so sánh với vòng lặp for ba phần mà bạn đã biết. Vòng lặp while tách ba việc đó ra: phần khởi tạo nằm trước vòng lặp, điều kiện nằm trong cặp ngoặc tròn, còn phần cập nhật nằm bên trong thân vòng lặp.
int i = 0; // initialization - before the loop
while (i < 5) { // condition - checked each pass
cout << i << "\n";
i++; // update - you must remember this yourself
}
Phần cuối cùng chính là chỗ dễ sai. Trong vòng lặp for, phần cập nhật nằm ngay cạnh điều kiện nên khó quên. Trong vòng lặp while, nó chỉ là một câu lệnh bình thường trong thân, và lỗi phổ biến nhất là bỏ sót nó khiến chương trình bị treo.
Một cái bẫy của C++ mà người mới hay mắc: một dấu chấm phẩy thừa ngay sau điều kiện biến thân vòng lặp thành một câu lệnh rỗng.
int i = 0;
while (i < 5); // <-- this semicolon is the entire loop body
{
cout << i << "\n";
i++;
}
Vòng lặp này chạy mãi mà không làm gì, vì ; chính là thân vòng lặp và i không bao giờ thay đổi. Khối lệnh trong cặp ngoặc nhọn sau đó chỉ chạy đúng một lần. Trình biên dịch sẽ không ngăn bạn: đây là code hoàn toàn hợp lệ, chỉ là không đúng ý bạn.
do-while: chạy thân vòng lặp ít nhất một lần
Đôi khi bạn cần thân vòng lặp chạy một lần trước rồi mới quyết định được có lặp lại hay không. Vòng lặp do-while kiểm tra điều kiện ở cuối, nên thân vòng lặp luôn chạy ít nhất một lần:
Bạn phải hỏi và đọc dữ liệu ít nhất một lần mới biết được dữ liệu đó có hợp lệ không, và do-while cho bạn đúng điều đó. Chú ý dấu chấm phẩy sau while (...): trong C++, dấu này là bắt buộc với do-while, và quên nó là một lỗi biên dịch rất hay gặp.
Sự khác biệt so với while thông thường thể hiện rõ khi điều kiện sai ngay từ đầu:
Chỉ có do-while body được in ra. Vòng lặp while bỏ qua hoàn toàn thân của nó vì x < 5 đã sai ngay trước lần kiểm tra đầu tiên.
Lặp đến khi cin thất bại
Một cách dùng while kinh điển là đọc dữ liệu đến khi hết: không có biến đếm, bạn cứ tiếp tục cho đến khi luồng nhập báo dừng. Trong C++, biểu thức cin >> value trả về chính luồng nhập đó, có giá trị truthy khi đọc thành công và falsy khi gặp cuối dữ liệu hoặc sai kiểu. Nhờ vậy nó trở thành một điều kiện vòng lặp rất gọn:
Điều kiện này làm hai việc cùng lúc: vừa đọc vừa kiểm tra trong một bước. Hãy so sánh với cách dùng giá trị sentinel, chẳng hạn dừng khi gặp 0. Khi đó bạn phải đọc một lần trước vòng lặp và đọc lại ở cuối mỗi lần lặp, để điều kiện luôn kiểm tra dữ liệu mới:
Nếu quên lệnh đọc thứ hai trong thân vòng lặp, value sẽ không bao giờ thay đổi và bạn có một vòng lặp vô hạn. Cách viết while (cin >> value) tránh được chuyện đó vì đưa luôn thao tác đọc vào điều kiện.
break và continue trong vòng lặp while
break và continue hoạt động giống hệt như trong vòng lặp for. break thoát khỏi vòng lặp ngay lập tức; continue nhảy thẳng về bước kiểm tra điều kiện, bỏ qua phần còn lại của lần lặp hiện tại:
Chương trình in ra 1 3 5 7 9. Vòng lặp while (true) được cố ý viết để không tự dừng: break là lối thoát duy nhất. Hãy cẩn thận với continue trong vòng lặp while: vì phần cập nhật nằm trong thân vòng lặp, nhảy về đầu bằng continue trước khi tăng biến đếm là một cách âm thầm làm treo vòng lặp. Trong ví dụ trên, n++ chạy trước nên không sao.
Cẩn thận với vòng lặp vô hạn
Điều kiện cuối cùng phải trở thành sai, và điều đó phụ thuộc hoàn toàn vào việc có thứ gì đó trong thân vòng lặp thay đổi. Hai thủ phạm quen thuộc là quên cập nhật, và cập nhật theo kiểu bước qua mất giá trị dừng:
int i = 0;
while (i < 5) {
cout << i << "\n"; // i never changes -> runs forever
}
int i = 0;
while (i != 10) {
i += 3; // 0, 3, 6, 9, 12... steps right past 10 -> runs forever
}
Ví dụ đầu bị treo vì i không bao giờ được cập nhật. Ví dụ thứ hai bị treo vì biến đếm bước qua đúng giá trị mà điều kiện đang tìm. Nên dùng < hoặc <= thay cho != khi bước nhảy có thể không rơi đúng vào giá trị đó. Một vòng lặp while (true) hoàn toàn ổn nếu nó chắc chắn có break; còn một vòng lặp vô tình như vậy thì chỉ là bug. Và hãy nhớ cái bẫy thân rỗng while (...); ở phần trước: một dấu chấm phẩy đặt sai chỗ tạo ra vòng lặp vô hạn mà thoạt nhìn trông hoàn toàn đúng.
Tiếp theo: vòng lặp for theo phạm vi
Vòng lặp while là công cụ phù hợp khi bạn lặp đến khi một điều kiện thay đổi và không có số lần lặp rõ ràng. Nhưng khi bạn chỉ muốn duyệt qua từng phần tử của một container (một vector, một mảng, một string) mà không cần chỉ số hay điều kiện nào, C++ hiện đại có cách gọn hơn: vòng lặp for theo phạm vi (range-based for, for (auto x : container)). Đó là nội dung của trang tiếp theo.
Câu hỏi thường gặp
while và do-while trong C++ khác nhau thế nào?
Vòng lặp while kiểm tra điều kiện trước lần lặp đầu tiên, nên thân vòng lặp có thể không chạy lần nào. Vòng lặp do-while chạy thân vòng lặp một lần trước, rồi mới kiểm tra điều kiện ở cuối, nên luôn chạy ít nhất một lần. Dùng do-while khi công việc phải được làm trước rồi mới quyết định có lặp lại hay không, chẳng hạn hỏi người dùng nhập dữ liệu rồi kiểm tra dữ liệu đó. Lưu ý do-while cần dấu chấm phẩy sau while (...) ở cuối.
Khi nào nên dùng vòng lặp while thay cho vòng lặp for trong C++?
Dùng vòng lặp while khi bạn không có một biến đếm rõ ràng mà chỉ muốn lặp đến khi một điều kiện thay đổi: đọc dữ liệu đến khi cin thất bại, chờ đến khi một giá trị sẵn sàng, hoặc xử lý một hàng đợi đến khi hết phần tử. Chọn vòng lặp for khi bạn biết trước số lần lặp hoặc có sẵn một chỉ số để đếm.
Làm sao để dừng vòng lặp while vô hạn trong C++?
Đảm bảo có thứ gì đó trong thân vòng lặp cuối cùng sẽ làm điều kiện thành false (giảm biến đếm, dịch con trỏ, đổi một biến cờ). Với vòng lặp while (true) viết có chủ đích, đặt một lệnh break bên trong, được bảo vệ bởi một câu lệnh if. Nếu vòng lặp bị treo, nguyên nhân thường gặp là quên cập nhật biến mà điều kiện phụ thuộc vào.