Menu

Toán tử trong C: số học, logic và độ ưu tiên

Mọi toán tử C đáng biết: số học, gán, so sánh, logic và tăng giảm, cùng cái bẫy chia số nguyên, cách phép chia lấy dư hoạt động với số âm, và một bảng độ ưu tiên bạn có thể tin cậy.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Toán tử (operator) là động từ của C: chúng nhận các giá trị và tạo ra giá trị mới. C có khoảng 40 toán tử, nhưng chỉ chừng một tá đã đủ cho phần lớn code, và hai trong số đó có những cái bẫy đủ nguy hiểm để đáng có phần riêng.

Toán tử số học

(%% trong chuỗi định dạng in ra một dấu phần trăm; nếu chỉ có % thì nó sẽ mở đầu một đặc tả định dạng.)

Năm toán tử số học hai ngôi là + - * / %, cộng thêm - một ngôi để đổi dấu và + một ngôi gần như không làm gì cả.

Cái bẫy chia số nguyên

a / b với hai số nguyên thực hiện phép chia số nguyên và bỏ đi phần dư. Không có làm tròn: kết quả bị cắt bớt về phía 0.

Trường hợp cuối cùng chính là bug lọt được tới môi trường production. Gán vào một biến double chẳng giúp gì: phép chia total / count đã tính xong, và đã là int, trước khi phép gán xảy ra. Cách sửa là biến một toán hạng thành số thực ngay bên trong biểu thức, thường bằng phép ép kiểu.

Hai quy tắc liên quan: chia số nguyên cho 0 là undefined behavior và thường làm chương trình crash, còn INT_MIN / -1 thì bị tràn số.

Toán tử chia lấy dư

% cho ra phần dư và chỉ dùng được với số nguyên:

Kiểm tra số chẵn bằng x % 2 == 0 và dùng % n để chỉ số quay vòng trong một mảng là hai cách dùng hằng ngày.

Với số âm, % của C lấy theo dấu của số bị chia:

Vì vậy x % 2 == 1 là một phép kiểm tra số lẻ bị sai khi x có thể âm: -7 % 2 bằng -1. Hãy dùng x % 2 != 0.

Với phần dư của số thực, % gây lỗi biên dịch; hãy dùng fmod() trong math.h.

Phép gán và phép gán kết hợp

= lưu một giá trị. Các dạng kết hợp gộp một phép toán với việc lưu:

x += 5 nghĩa là x = x + 5, nhưng nó chỉ tính x một lần, điều này quan trọng khi đích là một thứ như arr[compute_index()].

Bản thân phép gán cũng là một biểu thức cho ra giá trị vừa gán, đó là lý do a = b = c = 0 chạy được (nó gán từ phải sang trái) và cũng là lý do lỗi nhầm = với == vẫn biên dịch mà không báo gì.

Tăng và giảm

++ cộng một, -- trừ một. Mỗi toán tử có dạng tiền tố và dạng hậu tố:

Khi đứng thành câu lệnh riêng, hai dạng có thể thay cho nhau, và i++ là lựa chọn quen thuộc trong phần đầu vòng lặp for chỉ vì thói quen chứ không vì lý do sâu xa nào.

Điều bạn tuyệt đối không được làm là dùng một biến nhiều lần trong biểu thức mà chính biến đó cũng bị thay đổi:

int i = 5;
int x = i++ + i++;      /* undefined behavior */
arr[i] = i++;           /* undefined behavior */
printf("%d %d", i++, i);/* undefined behavior */

C không quy định thứ tự tính các biểu thức con đó, nên kết quả không chỉ đơn giản là không xác định: hành vi của cả chương trình là undefined. Tách câu lệnh làm hai thì sự mơ hồ biến mất.

Toán tử so sánh

Có sáu toán tử, và tất cả đều cho ra một int: 1 cho đúng và 0 cho sai.

Việc kết quả là một int bình thường chứ không phải một kiểu boolean riêng là một đặc trưng của C. Xem bài boolean trong C để biết những hệ quả của nó, kể cả bug nhầm = với ==.

Có hai lỗi so sánh chỉ riêng C mới có. So sánh nối tiếp không hoạt động như trong toán học: if (1 < x < 10) luôn đúng, vì 1 < x cho ra 0 hoặc 1 rồi kết quả đó mới được so với 10. Hãy viết if (x > 1 && x < 10). Và so sánh chuỗi bằng == là so sánh con trỏ chứ không phải nội dung: hãy dùng strcmp trong string.h.

Toán tử logic

&& là AND, || là OR, ! là NOT. Cả ba đều coi mọi giá trị khác 0 là đúng.

Chúng đánh giá ngắn mạch (short-circuit): && dừng ngay khi một toán hạng sai, còn || dừng ngay khi một toán hạng đúng. Phần còn lại không bao giờ được tính, và đây không chỉ là một cách tối ưu: đó là một đảm bảo bạn có thể dựa vào để giữ an toàn:

Đổi thứ tự hai phép kiểm tra đó thì lời gọi với NULL sẽ dereference một con trỏ null và crash. Kiểm tra trước, dùng sau: thứ tự trong một chuỗi && là một phần của logic.

Toán tử điều kiện

Toán tử ba ngôi duy nhất của C chọn giữa hai giá trị:

condition ? value_if_true : value_if_false. Đây là một biểu thức, nên nó đặt được ở những chỗ câu lệnh if không đặt được: trong một đối số của printf, trong một giá trị khởi tạo. Hãy viết ngắn gọn; toán tử điều kiện lồng nhau rất nhanh trở nên khó đọc.

Sơ lược về toán tử bit

Các toán tử này làm việc trên từng bit của một số nguyên:

Chúng được dùng cho cờ, mặt nạ bit và thanh ghi phần cứng. Có hai lưu ý: & và | không phải && và || (chúng không đánh giá ngắn mạch và làm việc theo từng bit), và dịch bit một giá trị có dấu hoặc dịch nhiều hơn độ rộng của kiểu là undefined. Hãy dùng kiểu không dấu khi làm việc với bit.

Độ ưu tiên

Khi biểu thức không có dấu ngoặc, bảng này quyết định cái gì gắn với cái gì. Độ ưu tiên cao nhất đứng đầu:

MứcToán tửTính kết hợp
1() [] -> . x++ x--trái sang phải
2! ~ ++x --x +x -x *p &x sizeof (type)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
10OR theo bittrái sang phải
11&&trái sang phải
12OR logictrái sang phải
13?:phải sang trái
14= += -= *= /= %= và các toán tử tương tựphải sang trái
15,trái sang phải

Hệ quả thực tế:

a + b * c       /* means a + (b * c) */
a < b == c < d  /* means (a < b) == (c < d) - rarely what you meant */
x & 1 == 0      /* means x & (1 == 0), i.e. x & 0 - a classic bug */
*p++            /* means *(p++) - dereference p, then advance it */

Dòng thứ ba đáng để thuộc lòng: == gắn chặt hơn &, nên phép kiểm tra mặt nạ bit cần dấu ngoặc: (x & 1) == 0.

Không ai nhớ hết mười lăm mức, và cũng chẳng ai cần nhớ. Chỉ cần biết * thắng +, phép so sánh thắng &&, && thắng ||, và mọi thứ còn lại thì thêm dấu ngoặc:

if ((flags & MASK) != 0 && (count > 0 || force)) { ... }

Dòng đó có những dấu ngoặc thừa, và nhờ vậy mà nó tốt hơn.

Câu hỏi thường gặp

Toán tử % trong C làm gì?

% là toán tử chia lấy dư (modulus): nó cho phần dư của phép chia số nguyên. 17 % 5 bằng 2. Nó chỉ dùng được với số nguyên: dùng với float hoặc double sẽ gây lỗi biên dịch, và fmod() trong math.h là phiên bản tương đương cho số thực.

Vì sao 5 / 2 trong C cho ra 2?

Vì cả hai toán hạng đều là số nguyên, nên C thực hiện phép chia số nguyên và bỏ phần thập phân. Để được 2,5, hãy cho một vế là số thực: 5 / 2.0, hoặc ép kiểu một toán hạng bằng (double)a / b.

i++ và ++i khác nhau thế nào?

Cả hai đều cộng thêm một vào i. ++i (tiền tố) tăng trước rồi cho ra giá trị mới; i++ (hậu tố) cho ra giá trị cũ rồi mới tăng. Khi đứng thành một câu lệnh riêng thì chúng như nhau: khác biệt chỉ xuất hiện khi kết quả được dùng, như int b = a++; so với int b = ++a;.

Độ ưu tiên toán tử trong C là gì?

Là thứ tự các toán tử gắn với toán hạng khi biểu thức không có dấu ngoặc. *, / và % gắn chặt hơn + và -, hai toán tử này gắn chặt hơn các phép so sánh, các phép so sánh gắn chặt hơn &&, rồi đến ||, rồi đến phép gán. Khi không chắc, hãy thêm dấu ngoặc: chúng chẳng tốn gì mà xóa luôn câu hỏi.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu