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

Độ ư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ứcToán tửKết hợp
1( ) [ ] . -> hậu tố ++ --Trái sang phải
2tiền tố ++ -- , + - một ngôi, ! ~ , (ép kiểu), * & , sizeofPhả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ácPhải sang trái
15, (toán tử phẩy)Trái sang phải
Mức 1 có độ ưu tiên cao nhất, mức 15 thấp nhất.

#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

Không nên
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. */
Nên
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ếtNghĩ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) * yMộ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ệmTrả lời câu hỏiCó được C quy định không
Độ ưu tiênCác toán hạng được nhóm với toán tử nàoCó, quy định chặt chẽ
Chiều kết hợpToán tử cùng mức thì nhóm theo chiều nàoCó, quy định chặt chẽ
Thứ tự đánh giáPhần nào của biểu thức được tính trướcPhầ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ử

  1. 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.
  2. Giải thích vì sao if (x & 1 == 0) không bao giờ đúng, rồi sửa lại.
  3. 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.
  4. Cho int x = a > b ? a : b + 1;, đoán kết quả với a = 3, b = 5 rồi kiểm chứng. Sau đó thêm ngoặc để nó cho ra kết quả ngược lại.
  5. 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.
  6. 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.