Menu

Boolean trong C: stdbool.h, _Bool và giá trị đúng sai

C không có kiểu boolean cho đến C99. Số 0 là sai, mọi giá trị khác là đúng, phép so sánh trả về int, và stdbool.h bổ sung bool, true, false. Kèm theo đó là lỗi nhầm = với == mà thiết kế này tạo ra.

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.

C đã sống 27 năm đầu tiên mà không có kiểu boolean. Đó không phải là sơ suất, mà là một quyết định thiết kế có những hệ quả bạn gặp ngay từ ngày đầu, cùng một lỗi nổi tiếng sinh ra trực tiếp từ nó.

Số 0 là sai, mọi giá trị khác là đúng

Quy tắc điều kiện của C không có giá trị đúng/sai đặc biệt nào. if, while, for, &&, || và ! đều chỉ hỏi đúng một câu: giá trị này có bằng 0 không?

Số âm là đúng. Số thập phân là đúng. Một con trỏ khác null là đúng. Chỉ có số 0, ở bất kỳ kiểu nào, là sai.

Đó là lý do code C đầy những cách viết trông rất cụt cho đến khi bạn biết quy tắc này:

if (count)            /* if count is not zero */
if (!count)           /* if count is zero */
if (p)                /* if p is not NULL */
if (!strcmp(a, b))    /* if the strings are equal - strcmp returns 0 for a match */

Trường hợp cuối hay làm người ta vấp. strcmp trả về 0 khi hai chuỗi giống nhau, nên trong if "bằng nhau" lại đọc thành "không". Viết if (strcmp(a, b) == 0) rõ ràng hơn mà kết quả hoàn toàn như nhau.

Phép so sánh trả về int

Trong C, phép so sánh không phải một loại biểu thức đặc biệt. Nó cho ra một int bình thường với giá trị 1 hoặc 0, và bạn có thể in nó ra, lưu nó lại hay dùng nó trong phép tính:

Toán tử ! cũng vậy: nó cho ra 1 nếu toán hạng bằng 0, và 0 trong các trường hợp còn lại. Vì thế !!x là một cách viết kinh điển để đưa mọi giá trị về đúng 0 hoặc 1.

bool, true và false (C99)

C99 bổ sung một kiểu boolean thật sự tên là _Bool, cùng một header đặt cho nó cái tên dễ đọc:

<stdbool.h> rất nhỏ: nó định nghĩa bool là _Bool, true là 1 và false là 0. Toàn bộ header chỉ có vậy. Nhưng dùng nó giúp ý định của code hiện rõ theo cách mà int flag = 1; không bao giờ làm được.

_Bool có đúng một hành vi thật sự khác biệt: nó chỉ lưu 0 hoặc 1. Gán bất kỳ giá trị khác 0 nào cũng được chuyển thành đúng 1:

printf vẫn không có %b cho boolean: hãy in bằng %d, hoặc tự chuyển sang dạng chữ:

Đến C23 mọi thứ còn đơn giản hơn: bool, true và false trở thành từ khóa thật sự, nên không cần include nữa. Phần lớn code hiện nay vẫn nhắm tới C17, vì vậy hãy giữ #include <stdbool.h>.

Trả về boolean từ một hàm

Đây là chỗ bool thể hiện giá trị rõ nhất:

Kiểu trả về bool cho nơi gọi hàm biết giá trị đó có nghĩa gì. Một hàm tên check_file trả về int có thể trả về boolean, một số đếm hoặc một mã lỗi, và theo quy ước của thư viện C thì thường là trường hợp thứ ba, với 0 nghĩa là thành công. Ghi rõ kiểu sẽ xóa bỏ chuyện phải đoán.

Lỗi nhầm = với ==

Vì phép gán là một biểu thức cho ra chính giá trị được gán, và vì mọi giá trị khác 0 đều là đúng, đoạn code sau vẫn biên dịch được:

Hai chuyện sai xảy ra cùng lúc. Điều kiện đã gán 5 cho x, xóa mất giá trị cũ, rồi kiểm tra 5, một giá trị khác 0, nên nhánh này chạy. Mặc định chương trình không có lỗi cũng không có cảnh báo nào.

Tệ hơn, phiên bản với 0 thì không bao giờ chạy nhánh của nó:

if (found = 0) { /* never taken - and found is now 0 */ }

Ba cách phòng tránh, xếp theo mức độ hữu ích:

Biên dịch với cảnh báo. -Wall bắt được lỗi này:

warning: suggest parentheses around assignment used as truth value

Đưa số cảnh báo về 0 mới là cách sửa thật sự, và chỉ tốn một cờ.

Viết hằng số trước. Kiểu "điều kiện Yoda" biến lỗi gõ nhầm thành lỗi biên dịch:

if (5 == x)   /* correct */
if (5 = x)    /* error: not an lvalue - the compiler stops you */

Có nhóm rất thích cách này, có nhóm thấy khó đọc; quan điểm nào cũng có lý, nhưng cách này đúng là hiệu quả.

Viết có chủ đích khi bạn thật sự muốn gán. Phép gán bên trong điều kiện rất hữu ích khi đọc đầu vào:

int c;
while ((c = getchar()) != EOF) { ... }

Cặp ngoặc thêm quanh c = getchar() là bắt buộc vì độ ưu tiên toán tử, đồng thời cho thấy phép gán là có chủ ý. Đó là cách viết chuẩn: nếu phép gán trong điều kiện của bạn không trông như vậy, nhiều khả năng đó là lỗi gõ nhầm.

Các lỗi thường gặp với boolean

So sánh với true. if (flag == true) chạy được với stdbool.h, nhưng sẽ sai ngay khi flag là một int mang giá trị 42, vì 42 == 1 là sai. Hãy viết if (flag).

So sánh nối chuỗi. if (0 < x < 10) luôn đúng: 0 < x cho ra 0 hoặc 1, và cả hai đều nhỏ hơn 10. Hãy viết if (x > 0 && x < 10).

Dùng toán tử bit thay cho toán tử logic. & và | không phải && và ||. Chúng không đánh giá tắt (short-circuit), và chúng kết hợp từng bit:

Cả hai toán hạng đều "đúng", vậy mà a & b lại bằng 0. Dùng & ở chỗ bạn định viết && tạo ra một điều kiện chỉ sai với một số giá trị nhất định, loại bug khó tìm nhất.

So sánh bằng nhau giữa số thực. if (0.1 + 0.2 == 0.3) là sai. Hãy so sánh trong một khoảng sai số cho phép; xem bài kiểu dữ liệu để hiểu vì sao.

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

C có kiểu boolean không?

Từ C99 thì có: _Bool là kiểu có sẵn, và khi include <stdbool.h> bạn có thêm các tên dễ đọc hơn là bool, true và false. Trước C99 hoàn toàn không có boolean: chương trình dùng int với 0 là sai và 1 là đúng, cách này đến nay vẫn chạy được và vẫn rất phổ biến.

Trong C, giá trị nào được coi là đúng?

Mọi giá trị khác 0. 1, -5, 0.01 và một con trỏ khác null đều là đúng trong điều kiện; chỉ 0, 0.0 và con trỏ null là sai. Không có phép kiểm tra boolean riêng: if (x) chỉ đơn giản kiểm tra x có khác 0 hay không.

Dùng bool trong C như thế nào?

Thêm #include <stdbool.h>, sau đó bool ready = true; hoạt động đúng như bạn mong đợi. Bên dưới, bool chính là _Bool, kiểu chỉ lưu 0 hoặc 1: gán bất kỳ giá trị khác 0 nào vào nó thì giá trị được lưu đúng bằng 1.

Khác nhau giữa = và == trong C là gì?

= là phép gán, == là phép so sánh. if (x = 5) gán 5 cho x rồi kiểm tra 5, một giá trị khác 0, nên nhánh này luôn chạy. if (x == 5) mới kiểm tra bằng nhau. Câu lệnh đầu vẫn biên dịch không lỗi, đó là lý do có -Wall và thói quen viết if (5 == x).

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