Chuyển tới nội dung chính

Toán tử và biểu thức

Một biểu thức (expression) là sự kết hợp của các giá trị, biến và toán tử (operator) để tạo ra một giá trị mới. Ví dụ a + b, 2 * n - 1, hay đơn giản chỉ một biến x cũng là biểu thức. Ngôn ngữ C cung cấp nhiều nhóm toán tử, bài này lần lượt điểm qua từng nhóm rồi xem máy tính tính một biểu thức từng bước ra sao.

Vì sao phải học kỹ toán tử, chẳng phải cộng trừ nhân chia ai cũng biết rồi sao? Vì máy tính không làm toán giống hệt như ta học ở phổ thông. Trên giấy, 7 / 23,5; trong C, nếu cả hai số đều nguyên thì kết quả là 3. Trên giấy, một số nguyên lớn bao nhiêu cũng viết được; trong C, một int chỉ chứa được tới khoảng hai tỉ, vượt qua là chuyện xấu xảy ra. Trên giấy, thứ tự đọc là từ trái sang phải; trong C, a < b < c không có nghĩa là "b nằm giữa a và c" mà là một biểu thức hoàn toàn khác và gần như luôn sai ý bạn muốn.

Những khoảng lệch đó là nơi sinh ra các lỗi khó chịu nhất với người mới: chương trình dịch trơn tru, chạy không báo gì, chỉ ra kết quả sai. Một bài toán tính điểm trung bình ra 4 thay vì 4.25, một vòng lặp chạy dư một lần, một điều kiện tưởng chặt nhưng lại luôn đúng. Học toán tử cho tới nơi chính là học cách đọc một biểu thức đúng như máy đọc, thay vì đúng như mắt mình muốn thấy.

Thử ngay: xem biểu thức chạy từng bước

Đoạn chương trình dưới tính biểu thức (double)(a + b * 2) / c với a = 3, b = 4, c = 2. Hãy bấm ▶ Chạy (hoặc kéo thanh trượt) để theo dõi từng giá trị biến biến đổi và từng phần của biểu thức được rút gọn dần. Bạn sẽ thấy máy không tính từ trái sang phải mà theo một trật tự riêng, lại còn đổi kiểu số giữa chừng; hai chuyện đó gọi là độ ưu tiên và ép kiểu, và các mục bên dưới sẽ giải thích cặn kẽ.

Tính biểu thức theo độ ưu tiên và ép kiểu C
1#include <stdio.h>
2
3int main(void) {
4 int a = 3, b = 4, c = 2;
5 int tong = a + b * 2;
6 double tb = (double)tong / c;
7 printf("tong = %d\n", tong);
8 printf("tb = %.2f\n", tb);
9 return 0;
10}
Ngăn xếp stack
main()
Bộ nhớ động heap
(trống)
Bắt đầu hàm main(). Chưa có biến nào được khai báo.
1/9

Toán tử số học

Gồm + (cộng), - (trừ), * (nhân), / (chia) và % (lấy phần dư, gọi là chia lấy dư, modulo). Hai toán tử cuối có những điểm dễ nhầm.

Ghi nhớ

Phép chia / của hai số nguyên cho ra một số nguyên: phần thập phân bị cắt bỏ chứ không làm tròn. Ví dụ 7 / 2 cho 3, không phải 3.5. Muốn ra 3.5 thì ít nhất một toán hạng phải là số thực, ví dụ 7.0 / 2.

Toán tử % chỉ áp dụng cho số nguyên và cho phần dư của phép chia: 7 % 2 cho 1, 10 % 5 cho 0. Phép này cực kỳ hữu ích, chẳng hạn để kiểm tra một số có chẵn không bằng cách xét n % 2 == 0.

Hai phép /% đi thành một cặp và nên được hiểu cùng nhau. Chia nguyên cho biết "có bao nhiêu lần trọn vẹn", còn chia lấy dư cho biết "còn thừa lại bao nhiêu". Từ cặp này ra rất nhiều thủ thuật thường dùng. Đổi 137 giây thành phút và giây: 137 / 60 cho 2 phút và 137 % 60 cho 17 giây. Lấy chữ số hàng đơn vị của n: n % 10. Bỏ chữ số cuối đi: n / 10. Từ hai phép đó, bạn đã đủ công cụ để tách một số nguyên thành từng chữ số bằng một vòng lặp. Kiểm tra chia hết cũng chỉ là n % k == 0.

#include <stdio.h>

int main(void) {
int giay = 137;
printf("%d phut %d giay\n", giay / 60, giay % 60); // 2 phut 17 giay

int n = 4058, tongChuSo = 0;
while (n > 0) {
tongChuSo += n % 10; // take the last digit
n = n / 10; // drop the last digit
}
printf("Tong chu so = %d\n", tongChuSo); // 4 + 0 + 5 + 8 = 17
return 0;
}

Hãy đọc kỹ vòng lặp trên. Với n = 4058, vòng đầu lấy 4058 % 10 bằng 8 cộng vào tổng, rồi 4058 / 10 cắt còn 405. Vòng sau lấy 5, cắt còn 40. Vòng sau nữa lấy 0, cắt còn 4. Vòng cuối lấy 4, cắt còn 0, và điều kiện n > 0 sai nên vòng lặp dừng. Chính phép chia nguyên (làm mất phần thập phân) mới khiến việc "cắt bớt chữ số" thành ra tự nhiên như vậy; nếu C chia ra số thực thì thủ thuật này sụp ngay.

Chia và lấy dư cho 0 làm chương trình chết

Toán học nói chia cho 0 là không xác định; C thì để chương trình chết thật. Cả a / 0 lẫn a % 0 trên số nguyên đều là hành vi không xác định, thường biểu hiện thành lỗi chia cho 0 làm chương trình tắt ngay lập tức. Kiểm tra mẫu số trước khi chia là bắt buộc, đặc biệt khi mẫu do người dùng nhập hoặc do một phép tính khác sinh ra. Với số thực thì khác: 1.0 / 0.0 thường không làm chết chương trình mà cho ra giá trị vô cực, in ra là inf, cũng chẳng phải điều bạn muốn.

Tràn số nguyên có dấu là hành vi không xác định

Một int thường chỉ chứa được từ khoảng âm hai tỉ tới dương hai tỉ. Nếu bạn cộng hoặc nhân vượt qua giới hạn đó, đừng nghĩ rằng số sẽ ngoan ngoãn quay vòng về số âm. Với số nguyên có dấu, tràn số là hành vi không xác định: trình biên dịch được phép giả định điều đó không bao giờ xảy ra, và mã sinh ra có thể cho kết quả kỳ quặc hoặc bỏ luôn cả một phép kiểm tra bạn viết ra. Chỉ số nguyên không dấu (unsigned) mới được chuẩn bảo đảm là quay vòng theo phép chia lấy dư. Tính giai thừa hay luỹ thừa mà thấy con số nhảy sang âm, đó là dấu hiệu bạn đã vượt giới hạn kiểu và cần dùng kiểu rộng hơn như long long.

Toán tử quan hệ và logic

Toán tử quan hệ (relational) dùng để so sánh: phép nhỏ hơn, lớn hơn, nhỏ hơn hoặc bằng, lớn hơn hoặc bằng, == (bằng) và != (khác). Kết quả của một phép so sánh là giá trị đúng hoặc sai. Trong C không có kiểu boolean riêng ở mức cơ bản: đúng biểu diễn bằng số 1, sai bằng số 0. Mọi giá trị khác 0 đều được coi là đúng.

ghi chú

Đừng nhầm = với ==. Dấu = là gán (đặt giá trị vào biến), còn == là so sánh bằng. Viết if (x = 5) sẽ gán 5 cho x rồi luôn nhận đúng, một lỗi rất khó tìm. Ý định đúng là if (x == 5).

Đừng viết a < b < c

Muốn nói b nằm giữa ac, nhiều bạn viết a < b < c như trong sách toán. C hiểu hoàn toàn khác. Vì phép so sánh kết hợp từ trái sang phải, máy tính a < b trước, ra 0 hoặc 1, rồi đem chính con số đó so tiếp với c. Với a = 5, b = 1, c = 3, biểu thức 5 < 1 cho 0, rồi 0 < 3 cho 1, tức là "đúng", dù rõ ràng 1 không nằm giữa 5 và 3. Cách viết đúng là nối hai điều kiện bằng và logic: a < b && b < c.

Toán tử logic (logical) kết hợp các điều kiện: && (và), || (hoặc), ! (phủ định). Ví dụ điều kiện n nằm trong khoảng 1 tới 10 viết là n >= 1 && n <= 10. Hai toán tử &&|| có tính chập mạch (short-circuit): nếu vế trái của && đã sai thì vế phải không cần tính nữa.

Chập mạch không chỉ để chạy nhanh

Nhiều người tưởng chập mạch chỉ là một mẹo tối ưu vặt. Thật ra nó là một công cụ bảo vệ, và C bảo đảm hành vi này chứ không phải tuỳ trình biên dịch. Với A && B, nếu A sai thì kết quả chắc chắn sai rồi, nên B không được phép tính. Với A || B, nếu A đúng thì kết quả chắc chắn đúng, nên B cũng không được tính. Nhờ vậy ta có thể đặt một phép kiểm an toàn ở vế trái để che chắn cho vế phải.

Ứng dụng kinh điển là kiểm mẫu số trước khi chia: if (b != 0 && a / b > 2). Nếu b bằng 0, vế trái sai, vế phải không bao giờ chạy, chương trình an toàn. Đổi hai vế cho nhau thành if (a / b > 2 && b != 0) là chết ngay khi b bằng 0, vì phép chia đã xảy ra trước khi phép kiểm kịp bảo vệ. Cũng vậy, với chuỗi ta viết if (n > 0 && s[n - 1] == '\n'), và với con trỏ ta viết if (p != NULL && *p > 0): vế trái luôn là tấm khiên, vế phải là việc chỉ được làm khi đã an toàn. Thứ tự hai vế của && vì thế không hề tuỳ tiện.

Một hệ quả nữa cần cảnh giác: nếu vế phải có tác dụng phụ, ví dụ x > 0 && dem++ > 3, thì dem sẽ tăng hay không tuỳ vào vế trái. Đây là nguồn lỗi rất khó tìm, nên tránh đặt phép tăng giảm hay lời gọi hàm có tác dụng phụ vào vế phải của toán tử logic.

Toán tử gán và gán phức hợp

Toán tử gán cơ bản là =. Ngoài ra C có các toán tử gán phức hợp viết gọn việc lấy chính nó rồi cập nhật: x += 3 tương đương x = x + 3. Tương tự có -=, *=, /=, %=. Chúng giúp mã ngắn và rõ ý hơn.

Điểm tinh tế là gán phức hợp chỉ tính vế trái một lần. Với biến đơn giản thì không thấy khác biệt, nhưng khi vế trái là một biểu thức phức tạp như a[i++] thì a[i++] += 5 chỉ tăng i một lần, còn a[i++] = a[i++] + 5 tăng hai lần và cho kết quả không đoán được. Ngoài ra bản thân phép gán cũng là một biểu thức có giá trị, chính là giá trị vừa gán, nên x = y = 0 chạy được: y = 0 cho ra giá trị 0 rồi giá trị ấy được gán tiếp cho x. Chính đặc tính "gán cũng là biểu thức có giá trị" này giải thích vì sao if (x = 5) không bị báo lỗi mà lặng lẽ luôn đúng.

Toán tử tăng giảm

Hai toán tử ++ (tăng 1) và -- (giảm 1) rất hay gặp, đặc biệt trong vòng lặp. Chúng có hai dạng: tiền tố (prefix) như ++i và hậu tố (postfix) như i++. Khi đứng riêng một câu lệnh, cả hai đều tăng i lên 1 và không khác gì nhau. Khác biệt chỉ xuất hiện khi dùng ngay trong một biểu thức lớn hơn: với ++i, biến được tăng trước rồi mới lấy giá trị; với i++, giá trị cũ được lấy trước rồi biến mới tăng.

Cách hiểu bền nhất là nghĩ theo hai câu hỏi: biến bị tăng lúc nào, và biểu thức nhận được con số nào. Cả hai dạng đều tăng biến, khác nhau chỉ ở con số mà biểu thức trả về. Với i đang bằng 3, viết x = i++; thì x nhận 3 (giá trị cũ) và i thành 4. Viết x = ++i; thì i thành 4 trước và x nhận 4. Vì lý do đó, printf("%d %d\n", i++, i++) là mã không nên viết: chuẩn C không quy định thứ tự tính các đối số, nên kết quả thay đổi theo trình biên dịch.

Đừng sửa cùng một biến hai lần trong một biểu thức

Những biểu thức kiểu i = i++; hay a[i] = i++; là hành vi không xác định trong C, không phải chỉ là khó đọc. Chuẩn ngôn ngữ không nói rõ phép tăng xảy ra trước hay sau phép gán, nên trình biên dịch có toàn quyền chọn, và hai trình biên dịch khác nhau có thể cho hai kết quả khác nhau mà không cái nào sai. Quy tắc an toàn: trong một câu lệnh, mỗi biến chỉ được thay đổi giá trị nhiều nhất một lần. Cần tăng thì tách ra thành câu lệnh riêng, mã vừa đúng vừa dễ đọc.

Độ ưu tiên và chiều kết hợp

Khi một biểu thức có nhiều toán tử, thứ tự tính không tuỳ tiện mà theo độ ưu tiên (precedence): toán tử ưu tiên cao tính trước. Giống như trong toán học, nhân chia trước cộng trừ, nên 2 + 3 * 4 cho 14 chứ không phải 20. Khi các toán tử cùng độ ưu tiên, ta theo chiều kết hợp (associativity), thường là từ trái sang phải.

MứcToán tửNhómChiều kết hợp
1 (cao nhất)() []ngoặc, chỉ sốtrái sang phải
2! ++ -- (ép kiểu)một ngôiphải sang trái
3* / %nhân chiatrái sang phải
4+ -cộng trừtrái sang phải
5phép so sánh thứ tựquan hệtrái sang phải
6== !=bằng, kháctrái sang phải
7&&và logictrái sang phải
8||hoặc logictrái sang phải
9 (thấp nhất)= += -= *=gánphải sang trái
mẹo

Khi nghi ngờ về thứ tự tính, đừng cố nhớ hết bảng. Hãy dùng dấu ngoặc để nói rõ ý mình. Viết (a + b) * c luôn rõ ràng hơn việc dựa vào trí nhớ về độ ưu tiên, và người đọc mã sau này cũng biết ơn bạn.

Hai khái niệm này khác nhau và hay bị gộp. Độ ưu tiên trả lời câu hỏi "toán tử nào ăn trước" khi các toán tử khác nhau đứng cạnh nhau, còn chiều kết hợp trả lời câu hỏi "gom từ bên nào" khi các toán tử cùng mức đứng cạnh nhau. Trong 10 - 4 - 3, cả hai dấu trừ cùng mức nên chiều kết hợp trái sang phải quyết định: máy gom thành (10 - 4) - 3 cho 3, chứ không phải 10 - (4 - 3) cho 9. Ngược lại, phép gán kết hợp từ phải sang trái, nên x = y = 0 được hiểu là x = (y = 0).

Bảng trên chỉ là phần thường dùng nhất trong hơn mười lăm mức ưu tiên của C, và không ai bắt bạn thuộc lòng nó. Điều cần nhớ nằm ở vài cặp hay gây tai nạn. Toán tử ! có ưu tiên rất cao, cao hơn cả ==, nên !x == 5 được hiểu là (!x) == 5 chứ không phải !(x == 5). Phép gán có ưu tiên thấp gần nhất, nên a = b > c nghĩa là a = (b > c), tức a nhận 0 hoặc 1. Còn && luôn ưu tiên cao hơn ||, nên a || b && c chính là a || (b && c), giống như nhân trước cộng.

Toán tử bit

Máy tính lưu số nguyên dưới dạng dãy bit, và C cho phép thao tác thẳng trên các bit đó. Nhóm này gồm & (và bit), | (hoặc bit), ^ (hoặc loại trừ), ~ (đảo bit), cùng hai phép dịch << (dịch trái) và >> (dịch phải). Chúng làm việc trên từng cặp bit tương ứng của hai toán hạng, khác hẳn &&|| vốn làm việc trên giá trị đúng sai của cả biểu thức.

Ví dụ với a = 12 (nhị phân 1100) và b = 10 (nhị phân 1010): a & b cho 1000 tức 8, vì chỉ bit nào đúng ở cả hai mới giữ lại; a | b cho 1110 tức 14; a ^ b cho 0110 tức 6, vì hoặc loại trừ chỉ giữ những bit khác nhau. Dịch trái một nhịp là nhân đôi, dịch phải một nhịp là chia đôi bỏ phần dư: 5 << 1 cho 10, còn 5 >> 1 cho 2.

#include <stdio.h>

int main(void) {
unsigned int a = 12u, b = 10u; // 1100 and 1010 in binary
printf("a & b = %u\n", a & b); // 8
printf("a | b = %u\n", a | b); // 14
printf("a ^ b = %u\n", a ^ b); // 6
printf("a << 1 = %u\n", a << 1); // 24, same as a * 2
printf("a >> 2 = %u\n", a >> 2); // 3, same as a / 4

unsigned int co = 0u;
co |= (1u << 2); // turn bit 2 on
printf("Bit 2 dang bat? %d\n", (co & (1u << 2)) != 0); // 1
return 0;
}

Trong lập trình hệ thống, nhóm toán tử này dùng để nhồi nhiều cờ đúng sai vào một số nguyên duy nhất cho tiết kiệm, hoặc để nói chuyện với phần cứng ở mức thanh ghi. Ở bước học này bạn chỉ cần nhận diện chúng và không nhầm & với &&. Một lời khuyên nhỏ: hãy dùng kiểu không dấu (unsigned) khi làm việc với bit, vì dịch phải một số âm cho kết quả phụ thuộc trình biên dịch, và dịch trái một số có dấu cho tràn số là hành vi không xác định.

Nhầm & với && và nhầm | với ||

if (x & 1)if (x && 1) trông na ná nhau nhưng làm hai việc khác hẳn: cái đầu lấy bit thấp nhất của x, cái sau kiểm x có khác 0 hay không. Vô tình gõ thiếu một ký tự, chương trình vẫn dịch được vì cả hai đều hợp lệ về cú pháp, và bạn sẽ săn con lỗi này rất lâu. Thêm nữa, toán tử bit có độ ưu tiên thấp hơn phép so sánh, nên x & 1 == 0 được hiểu là x & (1 == 0), tức x & 0, luôn cho 0. Ý đúng phải viết là (x & 1) == 0.

Ép kiểu

Ép kiểu (type casting) là việc chuyển một giá trị từ kiểu dữ liệu này sang kiểu khác, ví dụ từ int sang double. Ép kiểu ngầm do trình biên dịch tự làm, chẳng hạn khi cộng một int với một double thì int được nâng lên double trước khi cộng. Ép kiểu tường minh do người lập trình tự viết, bằng cách đặt tên kiểu trong ngoặc trước giá trị, như (double)a.

Quay lại phép chia số nguyên. Nếu ab đều là int, biểu thức a / b cho thương nguyên. Muốn có kết quả thực, ta ép một toán hạng sang double: (double)a / b.

Quy tắc mà trình biên dịch dùng khi hai toán hạng khác kiểu gọi là nâng kiểu (promotion): kiểu hẹp hơn được nâng lên cho khớp kiểu rộng hơn, theo thứ tự đại khái là int lên long, lên float, lên double. Phép tính rồi mới được thực hiện trên kiểu chung đó. Điều quan trọng phải nắm là việc nâng kiểu diễn ra theo từng phép toán, chứ không phải theo đích đến của kết quả. Đó là lý do double tb = tong / soMon; vẫn cho kết quả cụt: phép chia hai int đã hoàn tất và mất phần thập phân từ trước, việc gán vào một biến double sau đó không cứu được gì. Ép kiểu phải đặt vào trước phép chia, ngay tại toán hạng, thì mới có tác dụng.

Chiều ngược lại, ép từ rộng về hẹp, luôn làm mất mát. Ép double sang int thì phần thập phân bị cắt bỏ chứ không làm tròn, nên (int)3.99 cho 3(int)(-3.99) cho -3. Muốn làm tròn đúng nghĩa cho số dương, một mẹo quen thuộc là (int)(x + 0.5), còn cách chuẩn mực là gọi round trong math.h. Ép một giá trị quá lớn về kiểu hẹp hơn cũng làm hỏng số, ví dụ một double bằng 1e10 không lọt vừa một int.

Ép kiểu là dao sắc

Ép kiểu tường minh chính là câu bạn nói với trình biên dịch: "tôi biết mình đang làm gì, đừng cảnh báo nữa". Nó rất hữu ích cho phép chia thực, nhưng cũng có thể bịt miệng đúng cái cảnh báo đang muốn cứu bạn. Chỉ ép khi bạn giải thích được vì sao phép chuyển đó an toàn.

Bài tập thực hành

Bài tập 1: chia nguyên và ép kiểu

Cho int tong = 17, soMon = 4;. Tính điểm trung bình theo hai cách: (double)tong / soMontong / soMon, rồi in cả hai với hai chữ số thập phân. Giải thích vì sao hai kết quả khác nhau.

Lời giải chi tiết

Khi cả tongsoMon đều là int, phép tong / soMon được làm trước theo kiểu nguyên, ra 4, rồi mới gán vào double nên phần thập phân đã mất từ trước. Ngược lại, (double)tong nâng 17 thành 17.0 ngay trước phép chia, buộc chia theo kiểu thực và ra 4.25.

#include <stdio.h>
int main(void) {
int tong = 17, soMon = 4;
double sai = tong / soMon; // integer division first -> 4.0
double dung = (double)tong / soMon; // cast first -> 4.25
printf("Sai: %.2f\n", sai);
printf("Dung: %.2f\n", dung);
return 0;
}

Ý chính: ép kiểu phải đặt trước phép chia thì mới có tác dụng; đặt kết quả vào double sau cùng là quá muộn.

Bài tập 2: tiền tố và hậu tố

Cho đoạn mã int i = 3; int x = i++ + ++i;. Hãy tự tính giá trị của xi sau khi chạy, rồi viết chương trình in ra để kiểm chứng.

Lời giải chi tiết

Đọc từ trái sang phải: i++ lấy giá trị cũ 3 rồi tăng i lên 4. Tiếp theo ++i tăng i lên 5 trước rồi lấy 5. Vậy x = 3 + 5 = 8i = 5.

#include <stdio.h>
int main(void) {
int i = 3;
int x = i++ + ++i; // 3 (old i) + 5 (pre-incremented i)
printf("x = %d, i = %d\n", x, i); // x = 8, i = 5
return 0;
}

Ý chính: hậu tố lấy giá trị cũ rồi mới tăng, tiền tố tăng trước rồi mới lấy; trộn cả hai trong một biểu thức rất khó đọc nên hãy tránh ở mã thật.

Bài tập 3: biểu thức quan hệ và logic

Viết chương trình nhập hai số nguyên a, b rồi in ra kết quả của các biểu thức a > b, a == b, và a >= 1 && b >= 1. Quan sát chương trình in ra 0 hay 1 và giải thích.

Lời giải chi tiết

Mỗi biểu thức quan hệ hoặc logic cho một giá trị nguyên: 1 nếu đúng, 0 nếu sai. In bằng %d để thấy rõ con số đó.

#include <stdio.h>
int main(void) {
int a, b;
printf("Nhap a va b: ");
scanf("%d %d", &a, &b);
printf("a > b la %d\n", a > b);
printf("a == b la %d\n", a == b);
printf("a >= 1 && b >= 1 la %d\n", a >= 1 && b >= 1);
return 0;
}

Ý chính: trong C, đúng là 1 và sai là 0; biểu thức logic && chỉ cho 1 khi cả hai vế đều đúng.

Bài tập 4: chập mạch làm tấm khiên

Viết chương trình nhập hai số nguyên ab, rồi in ra thương a / b nếu chia được. Chương trình không được chết khi người dùng nhập b bằng 0. Hãy tận dụng tính chập mạch của &&.

Lời giải chi tiết

Điểm mấu chốt là đặt phép kiểm b != 0vế trái của &&. Khi b bằng 0, vế trái sai, C bảo đảm vế phải không được tính, nên phép chia không bao giờ xảy ra và chương trình an toàn. Nếu đảo hai vế thành a / b > 0 && b != 0 thì phép chia chạy trước, chương trình chết ngay trước khi phép kiểm kịp làm gì.

#include <stdio.h>

int main(void) {
int a, b;
printf("Nhap a va b: ");
if (scanf("%d %d", &a, &b) != 2) { // check the input first
printf("Du lieu khong hop le\n");
return 1;
}

// b != 0 is on the LEFT, so the division is never evaluated when b is 0
if (b != 0 && a / b > 2) {
printf("Thuong nguyen %d lon hon 2\n", a / b);
} else if (b == 0) {
printf("Khong the chia cho 0\n");
} else {
printf("Thuong nguyen %d khong lon hon 2\n", a / b);
}
return 0;
}

Chú ý nhánh else if (b == 0) được tách riêng: nhờ vậy trong nhánh else cuối cùng ta chắc chắn b khác 0 nên phép chia ở đó cũng an toàn. Đây là thói quen tốt, đừng bao giờ để một phép chia nằm ở chỗ mà bạn không chứng minh được mẫu số khác 0.

Bài tập 5: đọc biểu thức đúng như máy đọc

Không chạy chương trình, hãy tự xác định giá trị của x trong từng dòng sau, rồi viết chương trình in ra để đối chiếu: int x1 = 17 % 5 * 2;, int x2 = 10 - 4 - 3;, int x3 = 1 || 0 && 0;, int x4 = (int)(7 / 2.0);.

Lời giải chi tiết

Dòng thứ nhất: %* cùng mức ưu tiên, kết hợp trái sang phải, nên máy gom thành (17 % 5) * 2 bằng 2 * 2 bằng 4. Dòng thứ hai: hai dấu trừ cùng mức, gom trái sang phải thành (10 - 4) - 3 bằng 3. Dòng thứ ba: && ưu tiên cao hơn || nên biểu thức là 1 || (0 && 0), và vì vế trái của || đã đúng, phần còn lại thậm chí không được tính, kết quả là 1. Dòng thứ tư: 7 / 2.0 có một toán hạng thực nên chia theo kiểu thực ra 3.5, rồi ép về int cắt phần thập phân còn 3.

#include <stdio.h>

int main(void) {
int x1 = 17 % 5 * 2; // (17 % 5) * 2 = 2 * 2 = 4
int x2 = 10 - 4 - 3; // (10 - 4) - 3 = 3
int x3 = 1 || 0 && 0; // 1 || (0 && 0) = 1
int x4 = (int)(7 / 2.0); // 3.5 truncated to 3
printf("%d %d %d %d\n", x1, x2, x3, x4); // 4 3 1 3
return 0;
}

Ý chính: mắt người đọc từ trái sang phải và hay đoán theo trực giác, còn máy đọc theo độ ưu tiên rồi mới tới chiều kết hợp. Khi thấy một biểu thức phải dừng lại suy nghĩ mới hiểu, hãy thêm dấu ngoặc cho người sau đỡ khổ.

Tự kiểm tra

Vì sao 7 / 2 cho 3 chứ không phải 3.5?

Vì cả hai toán hạng 72 đều là số nguyên, nên / thực hiện phép chia nguyên: phần thập phân bị cắt bỏ. Muốn ra 3.5, ít nhất một toán hạng phải là số thực, ví dụ 7.0 / 2 hoặc (double)7 / 2.

Trong 2 + 3 * 4, phần nào được tính trước và kết quả là bao nhiêu?

Nhân có độ ưu tiên cao hơn cộng, nên 3 * 4 được tính trước ra 12, sau đó 2 + 12 cho 14. Nếu muốn cộng trước thì phải viết (2 + 3) * 4 để ra 20.

Câu hỏi tự kiểm

Kiểm tra nhanh: toán tử và biểu thức0/4 đúngchưa trả lời
  1. 1Trong C, biểu thức 7 / 2 với cả hai toán hạng là int cho kết quả nào?
  2. 2Muốn nói b nằm giữa a và c, bạn viết a < b < c. C hiểu biểu thức này thế nào?
  3. 3Vì sao không nên viết biểu thức như int x = i++ + ++i; trong C?
  4. 4Do độ ưu tiên của toán tử, biểu thức x & 1 == 0 được C hiểu là gì?

Tóm tắt

Toán tử trong C chia thành các nhóm số học, quan hệ, logic, gán, tăng giảm và thao tác bit. Chỗ khác biệt lớn nhất so với toán học phổ thông nằm ở số nguyên: chia hai int cho thương nguyên và cắt bỏ phần thập phân, % cho phần dư, chia cho 0 làm chương trình chết, và tràn số nguyên có dấu là hành vi không xác định chứ không phải quay vòng như nhiều người tưởng.

Các phép so sánh cho giá trị 1 hoặc 0, và hai toán tử && với || có tính chập mạch: vế trái sai (hoặc đúng) là đủ để bỏ qua vế phải, nên ta đặt phép kiểm an toàn ở vế trái để che chắn cho vế phải. Trong một biểu thức nhiều toán tử, máy tính theo độ ưu tiên rồi tới chiều kết hợp, và không bao giờ nên sửa cùng một biến hai lần trong một câu lệnh. Cuối cùng, ép kiểu phải đặt ngay tại toán hạng trước phép tính, vì việc nâng kiểu diễn ra theo từng phép toán chứ không theo kiểu của biến nhận kết quả.

Ghi nhớ

Toán tử C chia thành các nhóm số học, quan hệ, logic, gán và tăng giảm. Khi một biểu thức có nhiều toán tử, máy tính tính theo độ ưu tiên rồi chiều kết hợp; dùng dấu ngoặc để nói rõ ý luôn an toàn hơn dựa vào trí nhớ. Chia hai số nguyên cho thương nguyên, nên cần ép kiểu sang double đúng chỗ (trước phép chia) khi muốn giữ phần thập phân.