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

Toán tử so sánh

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

  • Giải thích vì sao kết quả so sánh trong C là kiểu int
  • Tránh năm lỗi so sánh kinh điển
  • So sánh số thực và chuỗi đúng cách
  • Giải thích vì sao không được trộn signed với unsigned

Sáu toán tử so sánh của C rất đơn giản, nhưng chúng đứng sau năm lỗi kinh điển mà gần như người học C nào cũng mắc ít nhất một lần. Bài này đi qua từng lỗi, giải thích nguyên nhân và cách phòng.

#Sáu toán tử

Toán tửVí dụKết quảÝ nghĩa
==5 == 51Bằng
!=5 != 31Khác
>5 > 31Lớn hơn
<5 < 30Nhỏ hơn
>=5 >= 51Lớn hơn hoặc bằng
<=3 <= 51Nhỏ hơn hoặc bằng

#1. Dùng dấu bằng đơn

Lỗi phổ biến nhất trong toàn bộ ngôn ngữ C.

Không nên
if (x = 5) {
    printf("x bằng 5");
}

/* Đây là phép GÁN, không phải so sánh.
   Nó gán 5 vào x, rồi lấy giá trị 5 làm điều kiện.
   5 khác 0 nên điều kiện LUÔN đúng.
   Và x đã bị thay đổi. */
Nên
if (x == 5) {
    printf("x bằng 5");
}

Lỗi này biên dịch được vì trong C, phép gán là một biểu thức có giá trị, và mọi biểu thức đều dùng làm điều kiện được.

#2. So sánh số thực

double a = 0.1 + 0.2;
double b = 0.3;

if (a == b) {
    printf("bằng nhau");     /* KHÔNG BAO GIỜ chạy */
}

Bài 2.3 đã giải thích nguyên nhân: số thực lưu ở hệ nhị phân nên 0,1 và 0,2 đều chỉ là giá trị gần đúng. Tổng của hai giá trị gần đúng không trùng khít với giá trị gần đúng của 0,3.

#include <math.h>

/* Cách 1: sai số tuyệt đối, đủ dùng khi các số cùng cỡ */
if (fabs(a - b) < 1e-9) {
    printf("coi như bằng nhau");
}

/* Cách 2: sai số tương đối, đúng cả với số rất lớn hoặc rất nhỏ */
int gan_bang(double x, double y, double eps)
{
    double lon_hon = fmax(fabs(x), fabs(y));

    return fabs(x - y) <= eps * lon_hon;
}

Riêng các phép so sánh lớn hơn và nhỏ hơn thì vẫn dùng bình thường được với số thực. Chỉ có phép bằng và khác là có vấn đề.

#3. So sánh chuỗi

Không nên
char a[10] = "abc";
char b[10] = "abc";

if (a == b) {
    printf("bằng nhau");     /* KHÔNG chạy */
}
/* a và b là hai mảng khác nhau, nằm ở hai địa chỉ khác nhau.
   Phép so sánh này so ĐỊA CHỈ, không so nội dung. */
Nên
#include <string.h>

char a[10] = "abc";
char b[10] = "abc";

if (strcmp(a, b) == 0) {
    printf("bằng nhau");     /* chạy */
}

#4. Trộn signed và unsigned

Đây là lỗi tinh vi nhất trong năm lỗi, vì đoạn code trông hoàn toàn hợp lý.

#include <stdio.h>

int main(void)
{
    int      a = -1;
    unsigned b = 1;

    if (a < b) {
        printf("a nhỏ hơn b\n");
    } else {
        printf("a KHÔNG nhỏ hơn b\n");   /* dòng này chạy */
    }

    return 0;
}

Trường hợp hay gặp nhất trong thực tế là so sánh với kết quả của sizeof hoặc strlen, vì cả hai đều trả về size_t không dấu.

if (-1 < sizeof(int)) {
    printf("không bao giờ in ra\n");
}

int n = -1;

if (n < strlen(s)) {
    printf("cũng không bao giờ in ra\n");
}

/* Vòng lặp vô hạn kinh điển */
for (size_t i = n - 1; i >= 0; --i) { }

#5. Nối chuỗi so sánh

Trong toán học, 0 < x < 10 nghĩa là x nằm giữa 0 và 10. Trong C, biểu thức đó hợp lệ về cú pháp nhưng có nghĩa hoàn toàn khác.

Không nên
if (0 < x && x < 10) { }   /* đúng */

if (0 < x < 10) { }
/* C hiểu là: ((0 < x) < 10)
   Phần trong ngoặc cho ra 0 hoặc 1.
   Cả 0 và 1 đều nhỏ hơn 10.
   Điều kiện LUÔN đúng với mọi x. */
Nên
if (0 < x && x < 10) {
    printf("x nằm trong khoảng");
}

/* Hoặc tách ra cho rõ ràng */
int trong_khoang = (x > 0) && (x < 10);

if (trong_khoang) { }

Bảng tổng kết năm lỗi

LỗiCách viết saiCách viết đúng
Dấu bằng đơnif (x = 5)if (x == 5)
So sánh số thựcif (a == b)if (fabs(a - b) < eps)
So sánh chuỗiif (s1 == s2)if (strcmp(s1, s2) == 0)
Trộn dấuif (n < strlen(s))if (n >= 0 && (size_t)n < strlen(s))
Nối chuỗi so sánhif (0 < x < 10)if (0 < x && x < 10)

Tự làm thử

  1. Viết hàm kiểm tra năm nhuận theo công thức (y % 4 == 0 && y % 100 != 0) || y % 400 == 0, kiểm thử với 1900, 2000, 2024, 2023.
  2. Nhập ba cạnh, kiểm tra có tạo thành tam giác không và nếu có thì thuộc loại nào.
  3. Kiểm tra một ký tự thuộc loại nào: chữ hoa, chữ thường, chữ số, khoảng trắng, hay ký tự đặc biệt. Không dùng <ctype.h>.
  4. Tái hiện lỗi trộn dấu: so sánh một biến int âm với sizeof(int), rồi biên dịch với -Wextra và đọc cảnh báo.
  5. Viết hàm gan_bang so sánh số thực theo sai số tương đối, kiểm thử với cặp số cỡ một tỷ và cặp số cỡ một phần tỷ.
  6. Tìm và sửa năm lỗi trong một đoạn code do bạn tự viết dựa trên bảng tổng kết ở trên.

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

  • Kết quả so sánh trong C là int, bằng 1 nếu đúng và 0 nếu sai.
  • if (x = 5) là phép gán và luôn đúng. Bật -Wall để bắt lỗi này.
  • Không so sánh số thực bằng dấu bằng kép, hãy so theo sai số tương đối.
  • So sánh chuỗi phải dùng strcmp. Dấu bằng kép so địa chỉ chứ không so nội dung.
  • Trộn signed với unsigned làm số có dấu bị đổi thành số không dấu rất lớn.
  • 0 < x < 10 luôn đúng, phải viết 0 < x && x < 10.