Bài 16 · Nâng cao · 26 phút· Cập nhật 11/06/2026
Bẫy kinh điển & undefined behavior
Biên soạn bởi Nguyễn Anh Tuấn
Bẫy kinh điển trong C (C Traps and Pitfalls): = vs ==, số octal, dangling else, ưu tiên toán tử - và undefined behavior (UB) kèm cách bắt bằng sanitizer.
Suốt 15 bài, bạn đã gặp lác đác những cái bẫy: chuỗi hằng không được sửa (Bài 5), con trỏ sau khi free (Bài 7), các bẫy con trỏ (Bài 10). Bài này gom chúng về một mối - theo cuốn sách mỏng nhất và nổi tiếng nhất về chủ đề này: C Traps and Pitfalls (Andrew Koenig, 1989). Koenig làm việc tại Bell Labs - chính nơi sinh ra C - và viết sách từ danh sách những lỗi mà các đồng nghiệp lão luyện của ông vẫn mắc.
Sách xếp bẫy theo tầng, giống như cách trình biên dịch đọc code của bạn:
- ▸Bẫy TỪ VỰNG (lexical): từng ký tự bị đọc khác ý bạn - dấu = không phải ==, số 010 không phải mười.
- ▸Bẫy CÚ PHÁP (syntactic): từng câu lệnh bị ghép khác ý bạn - else thuộc về if nào, dấu ; thừa, toán tử nào tính trước.
- ▸UNDEFINED BEHAVIOR: code hợp lệ về cú pháp nhưng chuẩn C không hứa gì về kết quả - tầng nguy hiểm nhất.
Vì sao đáng học riêng một bài
Bẫy nổi tiếng nhất của C chỉ là thiếu một ký tự. Trong C, phép gán x = 5 là một biểu thức CÓ GIÁ TRỊ (bằng 5), nên đặt nó vào if hoàn toàn hợp lệ:
gan-trong-if.c - biên dịch chạy bình thường; -Wall sẽ cảnh báo -Wparentheses đúng dòng này
#include <stdio.h>
int main(void) {
int x = 0;
if (x = 5)
printf("vao nhanh if! x = %d\n", x);
return 0;
} Kết quả khi chạy
vao nhanh if! x = 5
x đang là 0, nhưng x = 5 GÁN 5 vào x rồi trả về 5 - khác 0 nghĩa là "đúng", thế là vào nhánh if. Bẫy thứ hai còn im lặng hơn - nó nằm ở cách C đọc chữ số đầu tiên:
so-bat-phan.c - không một cảnh báo nào, kể cả với -Wall -Wextra
#include <stdio.h>
int main(void) {
int ngay = 010; /* dinh ghi ngay 10 */
printf("ngay = %d\n", ngay);
return 0;
} Kết quả khi chạy
ngay = 8
- ▸Hằng số nguyên bắt đầu bằng 0 là hệ BÁT PHÂN (octal): 010 = 8, 020 = 16 - di sản từ thời Unix.
- ▸Bẫy = vs == có cảnh báo nếu bật -Wall; bẫy octal thì KHÔNG - hợp lệ tuyệt đối, chỉ có mắt người cứu được.
- ▸Chỗ hay dính octal nhất: căn cột số cho thẳng hàng bằng số 0 đứng đầu.
Tầng thứ hai: từng từ đọc đúng cả, nhưng cách chúng ghép thành câu thì khác ý bạn. Thủ phạm quen nhất là một dấu ; thừa:
cham-phay-thua.c - ĐỪNG chạy: kẹt vô hạn, không bao giờ in (dấu ; sau while là thân vòng lặp rỗng)
#include <stdio.h>
int main(void) {
int i = 1;
while (i <= 3); /* <-- dau ; nay la THAN vong lap */
i++; /* thut le chi lua mat nguoi */
printf("%d\n", i);
return 0;
} Thụt lề không có ý nghĩa với C - và đó cũng là gốc của bẫy dangling else: else luôn gắn với if gần nhất chưa có else, bất kể bạn thụt lề thế nào:
dangling-else.c - người viết muốn else thuộc if ngoài, C nghĩ khác (-Wall cảnh báo -Wdangling-else)
#include <stdio.h>
int main(void) {
int diem = 4;
if (diem >= 0)
if (diem >= 5)
printf("Dau\n");
else
printf("Diem am?!\n");
return 0;
} Kết quả khi chạy
Diem am?!
Điểm 4 không hề âm, nhưng else thuộc về if (diem >= 5) - và 4 < 5 nên nhánh else chạy. Bẫy cú pháp cuối cùng là thứ tự ưu tiên toán tử, Koenig xếp vào nhóm kinh điển nhất:
uu-tien.c - kiểm tra số chẵn bằng bit cuối… nhưng == tính trước &
#include <stdio.h>
int main(void) {
int x = 6; /* 6 la so chan */
if (x & 1 == 0)
printf("chan\n");
else
printf("le?!\n");
return 0;
} Kết quả khi chạy
le?!
- ▸== có ưu tiên CAO hơn &, nên x & 1 == 0 nghĩa là x & (1 == 0) = x & 0 = 0 - "sai" với mọi x.
- ▸Quy tắc sống còn: trộn toán tử bit (& | ^ << >>) với so sánh thì LUÔN bọc ngoặc - (x & 1) == 0.
- ▸Cả ba bẫy cú pháp trên đều có cảnh báo nếu bật -Wall - thêm một lý do để không bao giờ biên dịch "trần".
Đến lượt bạn. Mỗi bẫy dưới đây là một chương trình nhỏ - đoán kết quả trước, chốt rồi mới hiện đáp án và lời giải. Có cả những bẫy chưa xuất hiện ở trên:
int x = 0;
if (x = 5)
printf("vao nhanh if! x = %d\n", x);Chương trình in gì?
Dự đoán trước rồi mới xem đáp án - chốt là không đổi được.
Trung thực
Hai bẫy cuối trong công cụ không có "kết quả đúng" để học thuộc - vì chuẩn C cố tình không định nghĩa hành vi của chúng. Undefined behavior (UB) nghĩa là: chương trình vi phạm một quy tắc mà chuẩn không bắt trình biên dịch phải kiểm tra, nên mọi kết quả đều "hợp lệ" - sập, số rác, hay đáng sợ nhất: chạy đúng hôm nay, hỏng ngày mai. Bản đồ các UB bạn sẽ gặp nhiều nhất:
| Undefined behavior | Ví dụ | Đã gặp ở |
|---|---|---|
| Tràn số nguyên có dấu | INT_MAX + 1 | bài này |
| Sửa & đọc cùng biến không thứ tự | a[i] = i++ | công cụ Bước 4 |
| Sửa chuỗi hằng | char *p = "hi"; p[0] = 'H'; | Bài 5 - Chuỗi & con trỏ |
| Dùng con trỏ sau free | free(p); *p | Bài 7 - Cấp phát động |
| Đọc biến chưa khởi tạo | int x; printf("%d", x); | bài này |
Không ai nhớ hết danh sách UB của chuẩn (hơn 200 mục) - nên người viết C chuyên nghiệp phòng thủ bằng công cụ, theo hai lớp:
- ▸Lớp 1 - lúc biên dịch: LUÔN bật cc -std=c17 -Wall -Wextra (dự án nghiêm túc thêm -Werror: cảnh báo = lỗi, không cho lờ đi).
- ▸Lớp 2 - lúc chạy: sanitizer. Thêm -fsanitize=undefined,address là chương trình tự tố cáo UB và lỗi bộ nhớ ngay tại dòng gây ra.
- ▸Quy tắc vàng: không bao giờ "thử xem máy in gì" để kết luận về UB - máy chỉ cho bạn MỘT khả năng trong vô số.
Sẽ học kỹ hơn
Bài tiếp theo
Câu hỏi thường gặp
Còn - vì các bẫy trong sách nằm ở THIẾT KẾ của ngôn ngữ, không phải ở một phiên bản trình biên dịch. = vs ==, dangling else, ưu tiên toán tử… hôm nay vẫn y nguyên như 1989. Thứ đã thay đổi là công cụ: trình biên dịch hiện đại cảnh báo được phần lớn các bẫy này - nếu bạn bật cờ.
Vì ba lý do: (1) không phải bẫy nào cũng có cảnh báo - 010 là số octal hợp lệ, im lặng tuyệt đối; (2) cảnh báo chỉ hiện khi bạn bật cờ, mà rất nhiều dự án không bật; (3) đọc code của người khác (review, debug) thì không có trình biên dịch nào đứng cạnh nhắc bạn.
Khác về mức độ. "Tuỳ trình biên dịch" (implementation-defined) nghĩa là mỗi trình biên dịch chọn MỘT cách và phải ghi vào tài liệu - vd kích thước int. Còn undefined behavior là chuẩn C buông tay hoàn toàn: chương trình có thể sập, ra số rác, hoặc đáng sợ nhất - chạy "đúng" hôm nay và hỏng vào lúc quan trọng nhất.
Đánh đổi lấy tốc độ. Nếu chuẩn bắt kiểm tra tràn số ở MỌI phép cộng, mọi chương trình C đều chậm đi. C chọn tin lập trình viên: "anh nói anh không tràn, tôi khỏi kiểm" - và trình tối ưu hoá dựa vào niềm tin đó để sinh mã nhanh. Đổi lại, khi bạn phá lời hứa, hậu quả không được định nghĩa.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
int t = 020; printf("%d", t); in ra số mấy?
- 1
Săn sạch 8 bẫy
Quay lại công cụ Bước 4: dự đoán đủ 8 bẫy. Bẫy nào đoán sai, đọc kỹ phần giải thích rồi kể lại bằng lời của mèo con.
Hoàn thành khi: Đã săn 8/8, và giải thích lại được 2 bẫy từng đoán sai mà không nhìn đáp án.
- 2
Bắt trình biên dịch khai
Chép đoạn
if (x = 5)ở Bước 2 vào máy, biên dịch 2 lần: không cờ, rồi với-Wall -Wextra. So hai kết quả.Hoàn thành khi: Không cờ: im lặng. Có cờ: cảnh báo
-Wparentheseschỉ đúng dòngif (x = 5). - 3
Bẫy im lặng nhất
Biên dịch
int t = 020; printf("%d", t);với-Wall -Wextra. Trình biên dịch có cảnh báo gì không? In ra số mấy?Hoàn thành khi: Không một cảnh báo nào; in 16 - vì
020là octal. Đây là bẫy hiếm hoi mà cờ không cứu được. - 4
Sửa dangling else
Lấy ví dụ
diem = 4ở Bước 3, thêm ngoặc nhọn đểelsethuộc vềifNGOÀI (ý đồ ban đầu), chạy lại.Hoàn thành khi: Với
diem = 4chương trình không in gì; vớidiem = -1in "Diem am?!". - 5
Tự gây tràn số có chủ đích
Viết chương trình cộng
INT_MAX + 1(cầnlimits.h), biên dịch vớicc -std=c17 -fsanitize=undefinedrồi chạy.Hoàn thành khi: UBSan in dòng "runtime error: signed integer overflow" - công cụ bắt được UB mà mắt thường bỏ qua.
- 6
Đọc sâu thêm (Koenig)
Nếu có cuốn C Traps and Pitfalls (giới thiệu ở bài 1 khoá C cơ bản): đọc chương 1 và 2 - bài này chỉ lấy một phần trong đó.
Hoàn thành khi: Tìm được ít nhất 1 bẫy trong sách mà bài này chưa nhắc, và tự giải thích được nó.