Bài 29. Thứ tự ưu tiên toán tử và thứ tự tính toán

Hiện mục lục Hiện mục lục

1. Thứ tự ưu tiên toán tử

Khi viết các biểu thức tính toán, người ta đề ra một số quy luật gộp các phần biểu thức của một số toán tử trước các toán tử khác để không phải lúc nào cũng phải viết tường minh các phần nào của biểu thức được gộp lại với nhau, làm cho biểu thức gọn gàng và dễ đọc hơn. Các quy luật đó được gọi là thứ tự ưu tiên toán tử (operator precedence). Các biểu thức tính toán trong C++ cũng áp dụng một bộ quy luật như vậy. Khi có nhiều toán tử xuất hiện trong một biểu thức, các quy luật quyết định một toán tử sẽ lấy các giá trị nào để thực hiện phép toán, đó có thể là một giá trị đơn lẻ nằm ngay bên cạnh hoặc kết quả của một toán tử khác.

Các toán tử đã được trình bày trong trang hướng dẫn này được sắp xếp theo các mức thứ tự ưu tiên giảm dần như sau:

Mức Các toán tử Chiều gộp
2 a++, a--,
a()
Trái sang phải →
3 ++a, --a,
+a, -a,
!a, ~a,
sizeof a
Phải sang trái ←
5 a * b, a / b, a % b Trái sang phải →
6 a + b, a - b Trái sang phải →
7 a << b, a >> b Trái sang phải →
9 a < b, a > b, a <= b, a >= b Trái sang phải →
10 a == b, a != b Trái sang phải →
11 a & b Trái sang phải →
12 a ^ b Trái sang phải →
13 a | b Trái sang phải →
14 a && b Trái sang phải →
15 a || b Trái sang phải →
16 c ? t : f,
a = b,
a += b, a -= b,
a *= b, a /= b, a %= b,
a <<= b, a >>= b,
a &= b, a ^= b, a |= b
Phải sang trái ←
17 a, b Trái sang phải →

Một số mức không có trong bảng thuộc về các toán tử khác chưa được giới thiệu.

Để xác định cách gộp các toán tử, tìm một nhóm toán tử có cùng mức ưu tiên và đứng liên tiếp nhau, với mức ưu tiên cao nhất và nằm xa nhất về phía bên trái. Nếu nhóm toán tử có chiều gộp từ trái sang phải, chọn toán tử xa nhất về bên trái trong nhóm; nếu nhóm toán tử có chiều gộp từ phải sang trái, chọn toán tử xa nhất về bên phải trong nhóm. Đặt một cặp ngoặc tròn () xung quanh toán tử và các giá trị xung quanh nó. Đối với toán tử nằm ở bên cạnh cặp ngoặc, giá trị của biểu thức bên trong cặp ngoặc sẽ là giá trị được đưa vào toán tử. Lặp lại quá trình đối với những toán tử còn lại chưa nằm trong ngoặc cho đến khi ta đặt một cặp ngoặc bao quanh toàn bộ biểu thức.

Ví dụ các bước gộp toán tử cho biểu thức a + 8 * b - 5 > c - -+d / 6 && e:

    a +  8 * b   - 5  >  c -   - +d   / 6    && e
    a +  8 * b   - 5  >  c -   -(+d)  / 6    && e
    a +  8 * b   - 5  >  c -  (-(+d)) / 6    && e
    a + (8 * b)  - 5  >  c -  (-(+d)) / 6    && e
    a + (8 * b)  - 5  >  c - ((-(+d)) / 6)   && e
   (a + (8 * b)) - 5  >  c - ((-(+d)) / 6)   && e
  ((a + (8 * b)) - 5) >  c - ((-(+d)) / 6)   && e
  ((a + (8 * b)) - 5) > (c - ((-(+d)) / 6))  && e
 (((a + (8 * b)) - 5) > (c - ((-(+d)) / 6))) && e
((((a + (8 * b)) - 5) > (c - ((-(+d)) / 6))) && e)

Và đến đây ta biết được mỗi toán tử sẽ nhận vào các giá trị nào.

Bạn có thể đặt những cặp ngoặc tròn () xung quanh những phần biểu thức bạn muốn gộp lại với nhau theo ý muốn trong mã nguồn mà không nhất thiết phải tuân theo thứ tự ưu tiên toán tử.

Một điều đặc biệt của toán tử ba ngôi c ? t : f là biểu thức t được coi như nằm trong một cặp ngoặc tròn.

2. Thứ tự tính toán

Thứ tự ưu tiên toán tử như đã trình bày ở trên không quyết định các toán tử nào sẽ được thực hiện tính toán trước. Ví dụ, trong biểu thức a * b + (c + d), mặc dù * có thứ tự ưu tiên trên +, nhưng điều đó không quyết định rằng a * b sẽ được tính trước c + d; thứ tự ưu tiên chỉ quyết định rằng kết quả của a * b sẽ được thực hiện cộng + với kết quả của (c + d).

C++ có một bộ quy luật để quyết định thứ tự tính toán trong một số trường hợp, một số quy luật liên quan đến các toán tử đã được trình bày trong trang hướng dẫn này như sau:

Danh sách đầy đủ của các quy luật có thể được xem trên CPPReference (https://en.cppreference.com/w/cpp/language/eval_order).

Nếu một phần biểu thức không rơi vào các quy luật thứ tự tính toán, việc thực hiện tính toán diễn ra không theo một trình tự xác định nào cả, thậm chí các phần nhỏ hơn có thể được thực hiện tính toán xen kẽ với nhau, thứ tự của lần thực hiện này có thể khác với thứ tự thực hiện của lần kế tiếp. Ví dụ trong biểu thức a + b * c, việc thực hiện tính toán a, bc có thể diễn ra trong bất kì thứ tự nào; một nửa quá trình tính a có thể được thực hiện trước, rồi quá trình tính c được thực hiện, rồi nửa còn lại của quá trình tính a được thực hiện nốt, rồi mới thực hiện quá trình tính b. Điều này cho phép bộ dịch tạo ra các đoạn mã máy thực hiện công việc tính toán một cách có hiệu quả nhất.

Do ảnh hưởng của điều trên, nếu hiệu ứng phụ của một biến không được sắp xếp trình tự đối với quá trình lấy giá trị hoặc một hiệu ứng phụ khác đối với cùng biến đó, chương trình sẽ là không hợp lệ và có hành vi không xác định. Ví dụ, ++i - i là không hợp lệ do hiệu ứng phụ của ++i không được sắp xếp trình tự đối với quá trình lấy giá trị của biến i làm toán hạng bên phải của toán tử -.

Nguy hiểm

Như vậy, nguồn nào nói rằng "thứ tự thực hiện biểu thức tuân theo thứ tự ưu tiên toán tử", "trong một chuỗi phép toán có cùng mức ưu tiên, các phép toán được thực hiện từ trái sang phải" hay "từ phải sang trái" là hoàn toàn sai. Một số ngôn ngữ khác như Java có quy định chặt chẽ hơn về thứ tự thực hiện biểu thức, với cái giá phải trả là hiệu quả tính toán không còn được như C++.

Như đã đề cập trong các bài trước, ta không nên đưa các thao tác gán biến vào trong một biểu thức tính toán, thay vào đó ta thực hiện gán trong các câu lệnh nằm riêng. Như vậy, một là ta tránh được nguy cơ chương trình trở nên không hợp lệ do thứ tự tính toán không xác định, hai là mã nguồn trở nên dễ đọc hơn.

Mục lục Đóng mục lục