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

do while

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

  • Phân biệt do while với while về thời điểm kiểm tra điều kiện
  • Dùng do while cho menu và cho việc kiểm tra dữ liệu nhập
  • Nhớ dấu chấm phẩy bắt buộc ở cuối
  • Giải thích vì sao macro nhiều câu lệnh dùng do while

do while chỉ khác while ở một điểm: điều kiện được kiểm tra sau khi thân đã chạy. Nghe nhỏ nhưng nó thay đổi hoàn toàn cách dùng, và cũng là lý do vòng lặp này xuất hiện trong mọi macro nhiều câu lệnh của C.

#Cú pháp

do {
    /* thân vòng lặp */
} while (dieu_kien);
dem.c
#include <stdio.h>

int main(void)
{
    int i = 1;

    do {
        printf("%d ", i);
        i++;
    } while (i <= 5);

    printf("\n");

    return 0;
}
terminal
gcc -std=c17 -Wall -Wextra dem.c -o dem && ./dem
1 2 3 4 5

#Khác biệt với while

Với dữ liệu bình thường thì hai vòng lặp cho kết quả giống nhau. Khác biệt chỉ lộ ra khi điều kiện sai ngay từ đầu.

int i = 100;

while (i < 10) {
    printf("A");
    i++;
}
/* In 0 lần */

i = 100;

do {
    printf("B");
    i++;
} while (i < 10);
/* In 1 lần, vì thân chạy trước khi kiểm tra */
whiledo while
Kiểm tra điều kiệnTrước thânSau thân
Số lần chạy tối thiểu01
Dấu chấm phẩy cuốiKhông
Dùng nhiều hay ítRất nhiềuÍt, chỉ vài tình huống

#Hai ứng dụng chính

do while phù hợp đúng một loại tình huống: khi công việc chắc chắn phải làm ít nhất một lần, còn có lặp lại hay không thì phụ thuộc kết quả của lần đó.

Ứng dụng 1: menu

menu.c
int chon;

do {
    printf("\n1. Thêm\n2. Xóa\n3. Tìm\n0. Thoát\nChọn: ");

    if (scanf("%d", &chon) != 1) {
        int ch;

        chon = -1;

        /* Phải kiểm tra cả EOF, nếu không thì khi luồng vào kết thúc
           vòng dọn bộ đệm này sẽ chạy vô hạn. */
        while ((ch = getchar()) != '\n' && ch != EOF) {
        }

        if (ch == EOF) {
            chon = 0;      /* hết dữ liệu vào thì thoát menu */
        }
    }

    switch (chon) {
        case 1: them(); break;
        case 2: xoa();  break;
        case 3: tim();  break;
        case 0: printf("Tạm biệt\n"); break;
        default: printf("Lựa chọn không hợp lệ\n"); break;
    }
} while (chon != 0);

Menu phải hiện ra ít nhất một lần trước khi người dùng có cơ hội chọn thoát. Dùng while ở đây sẽ phải khởi tạo chon bằng một giá trị giả để điều kiện đúng ngay lần đầu, khá gượng ép.

Dùng while, phải khởi tạo giả
int chon = -1;      /* giá trị giả chỉ để vào được vòng lặp */

while (chon != 0) {
    in_menu();
    scanf("%d", &chon);
    xu_ly(chon);
}
Dùng do while, tự nhiên hơn
int chon;           /* không cần giá trị giả */

do {
    in_menu();
    scanf("%d", &chon);
    xu_ly(chon);
} while (chon != 0);

Ứng dụng 2: hỏi lại tới khi dữ liệu hợp lệ

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

long doc_so(const char *nhac, long min, long max)
{
    char dong[128];
    long v;
    int  hop_le;

    do {
        printf("%s", nhac);

        if (!fgets(dong, sizeof dong, stdin)) {
            exit(EXIT_FAILURE);
        }

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

        char *end;

        v = strtol(dong, &end, 10);
        hop_le = (end != dong) && (*end == '\0') && (v >= min) && (v <= max);

        if (!hop_le) {
            printf("Giá trị phải nằm trong khoảng %ld đến %ld.\n", min, max);
        }
    } while (!hop_le);

    return v;
}

Lần hỏi đầu tiên chắc chắn phải xảy ra, nên do while diễn đạt đúng ý định hơn. Đây là biến thể của hàm nhập số an toàn ở Bài 3.4 và Bài 3.5, viết lại bằng do while thay cho lệnh for vô hạn đã dùng ở Bài 3.5.

#Dùng trong macro

Có một chỗ mà do while gần như bắt buộc, và bạn đã gặp nó ở Bài 2.5: gói nhiều câu lệnh trong một macro.

#define HOAN_DOI(a, b)      \
    do {                     \
        int t = (a);         \
        (a) = (b);           \
        (b) = t;             \
    } while (0)
Chỉ dùng ngoặc nhọn
#define HOAN_DOI(a, b) { int t = (a); (a) = (b); (b) = t; }

if (x > y)
    HOAN_DOI(x, y);
else
    printf("đã đúng thứ tự");

/* Bung ra thành:
   if (x > y)
       { ... } ;          <- dấu chấm phẩy thành câu lệnh rỗng
   else                   <- else giờ không có if đi kèm
       printf(...);
   Lỗi biên dịch: else without a previous if */
Dùng do while (0)
#define HOAN_DOI(a, b) \
    do { int t = (a); (a) = (b); (b) = t; } while (0)

if (x > y)
    HOAN_DOI(x, y);
else
    printf("đã đúng thứ tự");

/* Bung ra thành:
   if (x > y)
       do { ... } while (0);
   else
       printf(...);
   Hoàn toàn hợp lệ. */

Đây là lý do bạn sẽ thấy do while (0) ở khắp nơi trong mã nguồn của nhân Linux và của mọi thư viện C nghiêm túc. Nó không phải mẹo lạ mà là chuẩn mực.

Tự làm thử

  1. Viết chương trình đoán số: máy nghĩ một số từ 1 tới 100, người chơi đoán, máy báo cao hay thấp. Dùng do while và đếm số lần đoán.
  2. Viết menu máy tính bỏ túi có bốn phép toán, thoát bằng lựa chọn 0, dùng do while.
  3. Cài hàm doc_so ở trên, kiểm thử với các đầu vào sai như chữ cái, chuỗi rỗng, và số ngoài khoảng.
  4. Viết cùng một chương trình bằng cả whiledo while, chỉ ra chỗ nào phải thêm giá trị khởi tạo giả.
  5. Định nghĩa macro HOAN_DOI chỉ bằng ngoặc nhọn, dùng nó trong cặp if else, đọc thông báo lỗi, rồi sửa bằng do while (0).

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

  • do while kiểm tra điều kiện sau thân nên luôn chạy ít nhất một lần.
  • Dấu chấm phẩy sau ngoặc của while là bắt buộc, đây là trường hợp duy nhất trong C.
  • Hai ứng dụng chính là menu và hỏi lại tới khi dữ liệu hợp lệ.
  • Macro nhiều câu lệnh phải bọc trong do { } while (0) để gói thành một câu lệnh và nuốt được dấu chấm phẩy.
  • Trong mã thực tế do while ít gặp, chỉ dùng khi công việc chắc chắn phải làm ít nhất một lần.