Độ ưu tiên toán tử
Sau bài này bạn sẽ làm được
- Tra được bảng độ ưu tiên và chiều kết hợp
- Giải thích năm bẫy độ ưu tiên hay gặp nhất trong C
- Biết khi nào bắt buộc phải thêm ngoặc
Độ ưu tiên quyết định biểu thức được nhóm lại như thế nào. Bạn không cần thuộc lòng cả bảng 15 mức, nhưng cần nhớ vài chỗ mà C xếp thứ tự khác với trực giác, vì đó là nơi sinh ra những lỗi chạy sai mà trình biên dịch không hề cảnh báo.
#Bảng độ ưu tiên
| Mức | Toán tử | Kết hợp |
|---|---|---|
| 1 | ( ) [ ] . -> hậu tố ++ -- | Trái sang phải |
| 2 | tiền tố ++ -- , + - một ngôi, ! ~ , (ép kiểu), * & , sizeof | Phải sang trái |
| 3 | * / % | Trái sang phải |
| 4 | + - | Trái sang phải |
| 5 | << >> | Trái sang phải |
| 6 | < <= > >= | Trái sang phải |
| 7 | == != | Trái sang phải |
| 8 | & | Trái sang phải |
| 9 | ^ | Trái sang phải |
| 10 | | | Trái sang phải |
| 11 | && | Trái sang phải |
| 12 | || | Trái sang phải |
| 13 | ?: | Phải sang trái |
| 14 | = += -= *= /= và các dạng gán khác | Phải sang trái |
| 15 | , (toán tử phẩy) | Trái sang phải |
#Chiều kết hợp
Khi hai toán tử cùng mức đứng cạnh nhau, chiều kết hợp quyết định cái nào được nhóm trước.
10 - 3 - 2 /* trái sang phải: (10 - 3) - 2 = 5, không phải 10 - (3 - 2) */
100 / 10 / 2 /* trái sang phải: (100 / 10) / 2 = 5 */
a = b = c /* phải sang trái: a = (b = c) */
!!x /* phải sang trái: !(!x) */
*p++ /* hậu tố ++ ở mức 1, * ở mức 2, nên là *(p++) */#Năm bẫy hay gặp nhất
Bẫy 1: toán tử bit thấp hơn so sánh
if (x & 1 == 0) {
printf("chẵn");
}
/* C hiểu là: x & (1 == 0)
tức là: x & 0
tức là: 0
Điều kiện LUÔN sai với mọi x. */if ((x & 1) == 0) {
printf("chẵn");
}Đây là bẫy phổ biến nhất và cũng nguy hiểm nhất, vì code biên dịch sạch và chỉ đơn giản là không bao giờ chạy vào nhánh đúng.
Bẫy 2: dịch bit thấp hơn cộng trừ
a << 1 + 2
/* C hiểu là: a << (1 + 2) tức là a << 3
Không phải: (a << 1) + 2 */Bẫy 3: toán tử một ngôi và con trỏ
| Viết | Nghĩa là | Kết quả |
|---|---|---|
| *p++ | *(p++) | Lấy giá trị rồi dịch con trỏ |
| (*p)++ | (*p)++ | Tăng giá trị mà p trỏ tới |
| *p.x | *(p.x) | Thường là lỗi, ý bạn là (*p).x |
| *s->p | *(s->p) | Mũi tên ở mức 1, cao hơn dấu sao |
| -x * y | (-x) * y | Một ngôi cao hơn hai ngôi |
Bẫy 4: gán có độ ưu tiên rất thấp
a = b > c
/* C hiểu là: a = (b > c)
nên a nhận 0 hoặc 1, không phải giá trị của b hay c. */
while (c = getchar() != EOF)
/* C hiểu là: c = (getchar() != EOF)
nên c chỉ nhận 0 hoặc 1, không phải ký tự đọc được.
Phải viết: while ((c = getchar()) != EOF) */Bẫy 5: toán tử ba ngôi thấp hơn mọi thứ
int x = a > b ? a : b + 1;
/* C hiểu là: a > b ? a : (b + 1)
Vì ?: ở mức 13, còn + ở mức 4.
Nếu ý bạn là (a > b ? a : b) + 1 thì phải ghi ngoặc. */
printf("%d\n", 1 + a > b ? 2 : 3);
/* C hiểu là: ((1 + a) > b) ? 2 : 3 */#Ưu tiên khác thứ tự đánh giá
Đây là điểm rất nhiều người hiểu nhầm, và nó liên quan trực tiếp tới hành vi không xác định đã học ở bài trước.
| Khái niệm | Trả lời câu hỏi | Có được C quy định không |
|---|---|---|
| Độ ưu tiên | Các toán hạng được nhóm với toán tử nào | Có, quy định chặt chẽ |
| Chiều kết hợp | Toán tử cùng mức thì nhóm theo chiều nào | Có, quy định chặt chẽ |
| Thứ tự đánh giá | Phần nào của biểu thức được tính trước | Phần lớn KHÔNG quy định |
int x = f() + g() * h();
/* Độ ưu tiên nói rõ: kết quả là f() + (g() * h())
Nhưng KHÔNG có gì đảm bảo f, g, h được gọi theo thứ tự nào.
Trình biên dịch có thể gọi h trước, rồi g, rồi f. */#Khi nào thêm ngoặc
Cụ thể, luôn thêm ngoặc trong năm trường hợp sau:
/* 1. Trộn toán tử bit với toán tử so sánh */
if ((x & MAT_NA) == 0) { }
/* 2. Trộn && với || */
if ((a && b) || (c && d)) { }
/* 3. Gán bên trong điều kiện */
while ((c = getchar()) != EOF) { }
/* 4. Toán tử ba ngôi lồng trong biểu thức khác */
int x = (a > b ? a : b) + 1;
/* 5. Mọi tham số trong macro */
#define BINH_PHUONG(x) ((x) * (x))Ngược lại, những trường hợp sau thì không cần ngoặc, vì độ ưu tiên trùng với trực giác toán học và mọi lập trình viên C đều đọc quen:
a + b * c /* ai cũng biết nhân trước cộng */
a < b && c < d /* so sánh trước, logic sau */
!a || b /* phủ định trước */
a++ + b /* tăng trước */Tự làm thử
- Tính bằng tay rồi chạy kiểm chứng, với
a = 5, b = 3, c = 2:a + b * c,(a + b) * c,a > b == 1,a & b | c,a << 1 + 1. - Giải thích vì sao
if (x & 1 == 0)không bao giờ đúng, rồi sửa lại. - Viết
while (c = getchar() != EOF)rồi chạy, quan sát vòng lặp vô hạn, sau đó sửa bằng ngoặc. - Cho
int x = a > b ? a : b + 1;, đoán kết quả vớia = 3, b = 5rồi kiểm chứng. Sau đó thêm ngoặc để nó cho ra kết quả ngược lại. - Viết một biểu thức dùng bốn hàm có in ra màn hình, biên dịch bằng GCC và Clang, so sánh thứ tự gọi hàm.
- Tìm trong bài tập cũ của bạn một biểu thức dài, tách nó thành nhiều câu lệnh với biến trung gian có tên rõ nghĩa.
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
- Toán tử bit có độ ưu tiên THẤP hơn toán tử so sánh, đây là bẫy phổ biến nhất.
- Toán tử gán gần như thấp nhất, nên phép gán trong điều kiện luôn cần ngoặc.
- Hậu tố
++ở mức 1 nên*p++nghĩa là*(p++). - Độ ưu tiên quyết định cách nhóm, không quyết định thứ tự tính toán.
- Chỉ bốn toán tử đảm bảo thứ tự đánh giá:
&&,||,?:và toán tử phẩy. - Khi nghi ngờ thì thêm ngoặc. Biểu thức quá phức tạp thì tách thành nhiều câu lệnh.