II.4. Biểu thức và các phép toán C
Biểu thức
Biểu thức là sự kết hợp giữa các toán hạng và toán tử theo một cách phù hợp để diễn đạt một công thức toán học nào đó. Các toán hạng có thể là hằng, biến, hay lời gọi hàm hay một biểu thức con. Các toán tử thuộc vào tập các toán tử mà ngôn ngữ hỗ trợ. Biểu thức được phát biểu như sau:
− Các hằng, biến, lời gọi hàm là biểu thức
− Nếu A, B là biểu thức và ⊗ là một phép toán hai ngôi phù hợp giữa A và B thì A⊗B là biểu thức.
− Chỉ những thành phần xây dựng từ hai khả năng trên là biểu thức.
M ột biểu thức phải có thể ước lượng được và trả về giá trị thuộc một kiểu dữ liệu cụ thể. Giá trị đó được gọi là giá trị của biểu thức và kiểu của giá trị trả về được gọi là kiểu của biểu thức, ví dụ một biểu thức sau khi ước lượng trả lại một số nguyên thì chúng ta nói biểu thức đó có kiểu nguyên (nói ngắn gọn là biểu thức nguyên).
Ví dụ : p = 0/2;
s = sqrt0*0*p-c));
trong đó a, b, c là 3 biến số thực.
Biểu thức logic trong C: theo như trên chúng ta nói thì biểu thức logic là biểu thức mà trả về kết quả kiểu logic. Nhưng trong ngôn ngữ lập trình C không có kiểu dữ liệu này (như boolean trong Pascal). Trong C sử dụng các số để diễn đạt các giá trị logic (‘đúng’ hay ‘sai’). Một giá trị khác 0 nếu được dùng trong ngữ cảnh là giá trị logic sẽ được coi là ‘đúng’ và nếu giá trị bằng 0 được xem là sai. Ngược lại một giá trị ‘sai’(chẳng hạn như giá trị của biểu thức so sánh sai (5==3)) sẽ trả lại số nguyên có giá trị 0, và giá trị của biểu thức (ví dụ như 5 < 8) ‘đúng’ sẽ trả lại một số nguyên có giá trị 1. Sau này chúng ta còn thấy không phải chỉ có các số được dùng để diễn đạt giá trị ‘đúng’ hay ‘sai’ mà một con trỏ có giá trị khác NULL (rỗng) cũng được coi là ‘đúng’, và giá trị NULL được xem là ‘sai’.
Các toán tử (phép toán) của ngôn ngữ C
a. Phép gán
Cú pháp <biến> = <giá trị>
Trong đó vế trái là tên một biến và vế phải là một biểu thức có kiểu phù hợp với kiểu của biến. Với phép gán hệ thống sẽ ước lượng giá trị của vế phải sau đó gán giá trị vào biến bên trái.
Ví dụ:
int a, b;
a = 5;
b = a +15;
Sự phù hợp kiểu giữa vế bên phải và bên trái được hiểu là hoặc hai vế cùng kiểu hoặc kiểu của biểu thức bên phải có thể được chuyển tự động (ép kiểu) về kiểu của biến bên trái theo quy tắc chuyển kiểu tự động của ngôn ngữ C là từ thấp tới cao:
char → int → long → double.
Tuy nhiên trong thực tế sự ép kiểu phụ thuộc vào chương trình dịch, một số chương trình dịch cho phép tự chuyển các kiểu số bên phải về kiểu cúa vế trái bằng mà không cần phải tuân theo quy tắc trên, bằng cách cắt bỏ phần không phù hợp. Ví dụ bạn có thể gán bên phải là số thực (float) vào vế trái là một biến nguyên (int), trường hợp này chương trình dịch sẽ cắt bỏ phần thập phân và các byte cao, nhưng kết quả có thể không như bạn mong muốn. Với C chúng ta có thể thực hiện gán một giá trị cho nhiều biến theo cú pháp:
<biến_1>=<biến_2> = ,..=<giá_trị>
với lệnh trên sẽ lần lượt gán <giá_trị> cho các biến từ phải qua trái.
b. Các phép toán số học
phép cộng +
phép trừ -
phép nhân *
phép chia /
phép chia lấy phần dư : %
c. Các phép toán so sánh (quan hệ)
Trong phần các kiểu dữ liệu chúng ta không có kiểu dữ liệu tương tự như boolean trong Pascal để biểu diễn các giá trị logic (true, false). Vậy kết quả các phép toán so sánh mà chúng ta thu được ‘đúng’, ‘sai’ là gì? Ngôn ngữ C dùng các số để biểu thị giá trị ‘đúng’ hay ‘sai’. Một số có giá trị bằng 0 nếu dùng với ý nghĩa là giá trị logic thì được xem là ‘sai’ ngược lại nếu nó khác 0 được xem là ‘đúng’. Thực sự thì các phép so sánh trên cũng đều trả về giá trị là số nguyên, nếu biểu thức so sánh là ‘sai’ sẽ có kết quả = 0, ngược lại nếu biểu thức so sánh là đúng ta thu được kết quả = 1.
d. Các phép toán logic
− Phép toán ! (phủ định):
Cú pháp: ! <toán_hạng>
với <toán_hạng> là biểu thức số nguyên hoặc thực, nếu <toán_hạng> có giá trị khác 0 thì kết quả sẽ =0 và ngược lại, nếu <toán_hạng> ==0 thì kết quả sẽ = 1.
− Phép toán && 0:
Cú pháp: <toán_hạng_1> && <toán_hạng_2>
trong đó 2 toán hạng là các biểu thức số, kết quả của phép toán này chỉ ‘đúng’ (!=0) khi và chỉ khi cả 2 toán hạng đều có giá trị ‘đúng’ (!=0).
− Phép toán || 0:
Cú pháp: <toán_hạng_1> || <toán_hạng_2>
trong đó 2 toán hạng là các biểu thức số, kết quả của phép toán này chỉ ‘sai’ (0) khi và chỉ khi cả 2 toán hạng đều có giá trị ‘sai’ (=0).
e. Các phép toán tích luỹ (gán số học)
Trong các biểu thức toán số học chúng ta rất hay gặp các biểu thức dạng như a = a +k, tức là chúng ta tăng a lên một lượng bằng k, hoặc như a = a << k, tức là dịch các bít của a sang trái k vị trí rồi lại gán vào a. Trong C có các phép toán thực hiện chức năng này và ta gọi là các phép toán tích luỹ.
Cú pháp chung: < đích> <tt>= <nguồn>;
Trong đó <đích> là một biến thuộc kiểu số nguyên hoặc thực, <nguồn> là một giá trị phù hợp. <tt> là dấu phép toán số học : +,-,*,/,%, <<,>>,&,|,^
Lưu ý: hai kí tự <tt> và dấu = phải viết liền và trở thành 1 dấu toán tử của ngôn ngữ C. Bằng cách dùng toán tử tích luỹ, trong một số trường hợp chúng ta có thể giảm sự phức tạp (về cách viết) của một biểu thức rất nhiều và như vậy sẽ giảm khả năng viết sai một cách đáng kể.
Ví dụ:
a[f(i)+b[j*2]-srtlen(s)] = a[f(i)+b[j*2]-strlen(s)] +6;
được viết thành a[f(i)+b[j*2]-strlen(s)] += 6;
f. Toán tử điều kiện
Cú pháp: <bt>?<gt1>:<gt2>
Trong đó <bt>, <gt1> và <gt2> là các biểu thức, nếu <bt> có giá trị ‘đúng’ thì kết quả của biểu thức là <gt1> ngược lại nếu <bt> có giá trị ‘sai’ thì biểu thức trả lại <gt2>.
Ví dụ: (a>b?a:b)
ý nghĩa của biểu thức trên là nếu a >b thì kết quả là a ngược lại là b, tức là trả lại giá trị lớn nhất của a và b.
g. Phép tăng và giảm 1
Với biểu thức dạng a = a + 1 hoặc a = a - 1 thì trong C có dạng khác viết ngắn gọn hơn bằng cách dùng toán tử ++ hoặc --.
Mỗi toán tử này lại có hai dạng khác nhau đó là toán tử viết trước toán hạng (gọi là toán tử trước) và dạng toán tử viết sau toán hạng 0.
Cú pháp:
<tên_biến> ++
++<tên_biến>
<tên_biến> --
--<tên_biến>
Ví dụ:
int a=5,b,c=2;
b= a++;
c = ++
a +b;
Ý nghĩa của ++ là tăng toán hạng (là biến) lên 1 đơn vị và -- là giảm toán hạng 1.
Sự khác nhau giữa toán tử trước và toán tử sau được minh hoạ bằng ví dụ sau:
a = 4; b = 2; c= b + a++ ;
thì sau khi thực hiện ta có c = 6 và a = 5
hay x = b++; thì b = 3 và x=2;
nhưng nếu a = 4; b = 2; c= ++a +b ;
thì sau khi thưc hiện ta có c = 7 và a = 5 và x = ++b thì x=3, b=3.
Như vậy bạn thấy sự khác nhau giữa x= b++ ;(1) và x=++b (2); là trong (1) giá trị của b được gán cho x trước khi nó được tăng 1, còn trong (2) thì tăng giá trị của b lên 1 sau đó mới gán b cho x. Tức là có thể hiểu:
x = b++ ; ⇔ { x = b; b = b+1;}
còn x = ++b ; ⇔ { b = b+1; x = b; }
Tương tự cho toán tử --;
x = b-- ; ⇔ { x = b; b = b - 1;} còn x = --b ; ⇔ { b = b -1; x = b; }
Vậy :
− Trong biểu thức đơn dạng a++, ++a, b--, --b thì ý nghĩa của toán tử trước và sau là như nhau (cùng là tăng hay giảm 1)
− Trong biểu thức nói chung mà có a++ 0 hay ++b 0 thì giá trị của a được sử dụng trong biểu thức trước khi a được tăng (giảm) 1, và giá trị của b được sử dụng sau khi b đã được tăng (giảm) 1.
Lưu ý:
Bạn có thể dùng kết hợp nhiều lần toán tử ++, -- với một biến. Vì ++,-- có cùng độ ưu tiên và được kết hợp từ phải sang trái do vậy các phép toán dạng ++++a, ----a là được phép trong khi đó a++++, a---- là không được phép.
h. Toán tử & - lấy địa chỉ
Các biến và hằng là các được lưu trong bộ nhớ và được cấp tại địa chỉ nào đó, toán tử & trả lại địa chỉ của một biến hay hằng.
Cú pháp: &<tên_biến> hoặc &<tên_hằng>
i. Toán tử * ( truy xuất giá trị qua con trỏ)
Phần trên chúng ta biết * là phép nhân, nhưng nó còn có ý nghĩa là toán tử 1 ngôi với chức năng lấy giá trị của một thành phần thông qua con trỏ.
Cú pháp: * <tên_con_trỏ>
Như vậy với một biến được cấp phát tại một vùng nhớ nào đó trong bộ nhớ thì chúng ta có thể truy xuất giái trị của nó thông qua tên biến hoặc qua địa chỉ (con trỏ) của nó. Giá sử pa là con trỏ và pa trỏ tới biến a (có kiểu phù hợp) thì *pa chính là giá trị của a. và cách truy xuất theo tên biến a hoặc qua con trỏ *pa là như nhau.
Ví dụ: int a, b, c;
int *p;
p=&a;
*p = 5;
b = a + 3;
c =*p -1;
Sau các lệnh trên thì a có giá trị là 5, b là 8 và c là 4 0.
j. Toán tử ,
Dấu phẩy (,) thường được dùng trong như dấu phân cách giữa các biến, các hằng được khai báo trên cùng một dòng, hoặc giữa các tham số của hàm. Trong một số trường hợp nào đó nó được dùng như một toán tử để tạo ra một biểu thức dạng A,B (với A, B là hai biểu thức con hợp lệ). Các biểu thức con được tính từ trái qua phải và giá trị của biểu thức con cuối (bên phải) chính là giá trị trả về của toàn biểu thức.
Ví dụ: a=5; b=6; c=2; x= 0; k ết quả x = 12 nh ưng nếu x =a+b,a*2+c; thì x =11.
k. Phép chuyển kiểu
Trong C cũng như một số ngôn ngữ lập trình khác, trong một biểu thức thì các toán hạng phải cùng kiểu. Tuy nhiên trong thực tế thì không thể cứng nhắc như vậy, chẳng hạn cộng một số nguyên với một số thực rõ ràng là khác kiểu nhưng bạn vẫn có thể thực hiện được. Thực ra thì trước khi thực hiện toán tử cộng đó chương trình dịch đã thực hiện thao tác chuyển đổi kiểu của số nguyên thành số thực chúng ta gọi là phép chuyển kiểu (ép kiểu). Trong một số tình huống việc chuyển kiểu trong C có thể được chương trình dịch thực hiện tự động (gọi là ép kiểu tự động) hoặc được ép kiểu kiểu tường minh 0. Nói chung sự chuyển kiểu tự động xảy ra trong bốn trường hợp sau:
− Các toán hạng trong một biểu thức khác kiểu.
− Gán một biểu thức vào một biến khác kiểu.
− Truyền tham số thực sự khác kiểu với tham số hình thức.
− Giá trị trả về của hàm sau câu lệnh return khác với kiểu hàm được khai báo.
Trong trường hợp thứ nhất quy tắc chuyển kiểu từ thấp lên cao được áp dụng, tức là toán hạng có kiểu thấp hơn sẽ được tự động chuyển thành kiểu của toán hạng cao hơn theo trật tự:
char ⇒ int ⇒ long ⇒ float ⇒ double
Trong ba trường hợp cuối kiểu của giá trị vế phải được chuyển theo kiểu của biến bên trái, kiểu các tham số thực sự được chuyển theo kiểu của tham số hình thức, kiểu giá trị trả về (sau return) phải chuyển thành kiểu của hàm.
L ưu ý là chỉ chuyển kiểu giá trị tức thời của toán hạng rồi thực hiện phép toán chứ kiểu của bản thân toán hạng thì không thay đổi. Trong một số yêu cầu chúng ta cần sự chuyển kiểu rõ ràng (ép kiểu tường minh) chứ không sử dụng quy tắc chuyển kiểu ngầm định, trong trường hợp này bạn có thể sử dụng toán tử chuyển kiểu theo cú pháp sau:
(ki ểu_mới) (biểu_thức)
trong cấu trúc này (kiểu_mới) là tên một kiểu hợp lệ nào đó, và giá trị của (biểu_thức) trả về bắt buộc phải chuyển thành (kiểu_mới)
Ví dụ 1:
int a=5, b =2;
float c;
c = (float) a /b;
thì c có giá trị =2.5. nhưng c= a/b ; thì c lại có giá trị =2.0
Ví dụ 2:
float a =7.0;
int b;
b = (int)a % 3;
Trong C yêu cầu phải dùng cặp ngoặc 0, ví dụ trong C++ bạn có thể dùng phép chuyển kiểu như int (a).
l. Độ ưu tiên các toán tử
Trong biểu thức có thể có nhiều toán tử, vậy điều gì giúp cho chương trình dịch thực hiện các toán tử một cách đúng đắn?. Trong các biểu thức nếu có các cặp (), thì nó sẽ quyết định thứ tự thực hiện các phép toán: trong ngoặc trước, ngoài ngoặc sau. Nhưng có những khả năng dấu ngoặc không có hoặc không đủ để quyết định tất cả các trường hợp thì khi đó C thực hiện các toán tử căn cứ vào độ ưu tiên của chúng và sử dụng một số quy tắc về các toán tử (ví dụ như khi chúng cùng độ ưu tiên thì thực hiện từ trái qua phải hay từ phải qua trái). Ví dụ với các phép toán số học +, - có cùng độ ưu tiên, nên nếu trong biểu thức có nhiều phép toán +, - và không có các dấu ngoặc quy định thứ tự thực hiện thì chúng sẽ được thực hiện từ trái qua phải. Nhưng với phép toán ++, hay các phép gán,.. chẳng hạn như ++++ a; hoặc a=b=c=d trình tự kết hợp lại từ phải qua trái.