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

Toán tử logic

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

  • Giải thích quy tắc 0 là sai và mọi giá trị khác 0 là đúng
  • Dùng đánh giá ngắn mạch để tránh chương trình dừng đột ngột
  • Phân biệt && với & và || với |
  • Rút gọn điều kiện phức tạp bằng luật De Morgan

Ba toán tử logic của C có một đặc tính mà nhiều người học không để ý: chúng không phải lúc nào cũng đánh giá cả hai vế. Đặc tính này không chỉ giúp chương trình nhanh hơn, nó còn là công cụ chính để tránh các phép truy cập nguy hiểm.

#Ba toán tử

Toán tửTênĐúng khi
&&Cả hai vế đều đúng
||HoặcÍt nhất một vế đúng
!Phủ địnhVế đó sai
int a = 1, b = 0;

printf("%d\n", a && b);   /* 0 */
printf("%d\n", a || b);   /* 1 */
printf("%d\n", !a);       /* 0 */
printf("%d\n", !b);       /* 1 */

Giống toán tử so sánh, kết quả của toán tử logic là int mang giá trị 0 hoặc 1.

#Quy tắc chân lý

Quy tắc duy nhất cần nhớ
Trong C, số 0 là sai, và mọi giá trị khác 0 đều là đúng. Không có ngoại lệ nào.
if (0)      { }   /* không chạy */
if (1)      { }   /* chạy */
if (42)     { }   /* chạy */
if (-1)     { }   /* chạy, số âm cũng khác 0 */
if (0.0)    { }   /* không chạy */
if (0.001)  { }   /* chạy */
if ('\0')  { }   /* không chạy, ký tự null có mã 0 */
if ('0')    { }   /* CHẠY, ký tự số 0 có mã 48 */

Nhờ quy tắc này, có nhiều cách viết tắt rất phổ biến trong mã C thực tế:

if (p)          /* tương đương if (p != NULL) */
if (!p)         /* tương đương if (p == NULL) */
if (n)          /* tương đương if (n != 0) */
if (!n)         /* tương đương if (n == 0) */

while (*s)      /* chạy tới khi gặp ký tự kết thúc chuỗi */
    ++s;

#Đánh giá ngắn mạch

Đây là phần quan trọng nhất của bài.

  • Với &&, nếu vế trái sai thì kết quả chắc chắn sai, nên vế phải không được đánh giá.
  • Với ||, nếu vế trái đúng thì kết quả chắc chắn đúng, nên vế phải không được đánh giá.
#include <stdio.h>

int co_in(const char *ten, int gia_tri)
{
    printf("Đã gọi %s\n", ten);

    return gia_tri;
}

int main(void)
{
    printf("--- 0 && ... ---\n");
    co_in("trái", 0) && co_in("phải", 1);   /* chỉ in "Đã gọi trái" */

    printf("--- 1 || ... ---\n");
    co_in("trái", 1) || co_in("phải", 1);   /* chỉ in "Đã gọi trái" */

    return 0;
}

Ứng dụng quan trọng nhất: bảo vệ trước khi truy cập

Không nên
if (p->data > 0 && p != NULL) {
    /* Chương trình dừng đột ngột nếu p là con trỏ rỗng.
       Vế trái được đánh giá TRƯỚC, và nó đã truy cập p->data rồi. */
}
Nên
if (p != NULL && p->data > 0) {
    /* An toàn: nếu p rỗng thì vế trái sai,
       vế phải không bao giờ được chạy. */
}

Cùng nguyên lý đó áp dụng cho nhiều tình huống khác:

/* Tránh chia cho 0 */
if (n != 0 && 100 / n > 5) { }

/* Kiểm tra chỉ số trước khi truy cập mảng */
if (i < n && a[i] == x) { }

/* Kiểm tra chuỗi rỗng trước khi đọc ký tự đầu */
if (s != NULL && s[0] != '\0') { }

/* Dừng sớm khi đã tìm thấy */
if (da_tim_thay || tim_kiem_rat_ton_kem()) { }

#Logic khác toán tử bit

C có hai cặp toán tử trông giống nhau nhưng làm hai việc hoàn toàn khác nhau. Nhầm lẫn giữa chúng là lỗi rất khó phát hiện vì code vẫn biên dịch được.

Toán tửLoạiCó ngắn mạch?Kết quả
&&Logic0 hoặc 1
&BitKhôngKết quả và từng bit
||Logic0 hoặc 1
|BitKhôngKết quả hoặc từng bit
!LogicKhông áp dụng0 hoặc 1
~BitKhông áp dụngĐảo từng bit
int a = 2, b = 4;

printf("%d\n", a && b);   /* 1: cả hai đều khác 0 */
printf("%d\n", a & b);    /* 0: 0010 và 0100 không có bit chung */

printf("%d\n", a || b);   /* 1 */
printf("%d\n", a | b);    /* 6: 0010 hoặc 0100 bằng 0110 */

printf("%d\n", !a);       /* 0 */
printf("%d\n", ~a);       /* -3: đảo mọi bit của 2 */

Toán tử bit sẽ được học kỹ ở Chương 31. Bây giờ chỉ cần nhớ: khi làm việc với điều kiện, luôn dùng &&||.

#Luật De Morgan

Hai đẳng thức này giúp bạn viết lại điều kiện phủ định thành dạng dễ đọc hơn.

!(A && B)   tương đương   (!A || !B)
!(A || B)   tương đương   (!A && !B)
Khó đọc
if (!(tuoi >= 18 && co_bang_lai)) {
    printf("Không được lái xe");
}

if (!(la_admin || la_chu_so_huu)) {
    printf("Không có quyền");
}
Dễ đọc hơn
if (tuoi < 18 || !co_bang_lai) {
    printf("Không được lái xe");
}

if (!la_admin && !la_chu_so_huu) {
    printf("Không có quyền");
}

Đơn giản hóa biểu thức logic

A && 1      ->   A
A && 0      ->   0
A || 1      ->   1
A || 0      ->   A
!!A         ->   A đổi về 0 hoặc 1
A && A      ->   A
A || !A     ->   1

Mẹo !!A đôi khi được dùng để ép một giá trị bất kỳ về đúng 0 hoặc 1, ví dụ khi bạn muốn lưu cờ vào một trường bit.

Tự làm thử

  1. Viết chương trình chứng minh ngắn mạch bằng cách dùng hàm có in ra màn hình ở cả hai vế của &&||.
  2. Viết hàm kiểm tra một điểm có nằm trong hình chữ nhật hay không, chỉ dùng toán tử logic.
  3. Cho biểu thức !(a > 5 && b < 10 || c == 0), viết lại bằng luật De Morgan sao cho không còn dấu phủ định ngoài cùng.
  4. Tái hiện lỗi dùng & thay cho && với con trỏ rỗng, chạy với -fsanitize=address và đọc báo cáo.
  5. Viết hàm kiểm tra mật khẩu hợp lệ: dài ít nhất 8 ký tự, có chữ hoa, có chữ thường, có chữ số. Dùng ngắn mạch để dừng sớm.
  6. Giải thích vì sao if (n != 0 && 100 / n > 5) an toàn còn if (100 / n > 5 && n != 0) thì không.

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

  • Trong C, số 0 là sai và mọi giá trị khác 0 đều là đúng.
  • && dừng khi vế trái sai, || dừng khi vế trái đúng. Đây gọi là đánh giá ngắn mạch.
  • Nhờ ngắn mạch, if (p != NULL && p->data > 0) an toàn còn thứ tự ngược lại thì không.
  • && khác &: toán tử bit không có ngắn mạch và thao tác trên từng bit.
  • Luật De Morgan giúp viết lại điều kiện phủ định thành dạng dễ đọc hơn.
  • Đừng đặt lời gọi hàm quan trọng ở vế phải của toán tử logic vì nó có thể không chạy.