🌱 Embedded C - Tìm hiểu về Toán tử Operators

🌱 Embedded C - Tìm hiểu về Toán tử Operators

    Trong các ngôn ngữ lập trình nói chung và C nói riêng, việc hiểu và sử dụng các toán tử - operators là rất quan trọng. Toán tử là các ký hiệu hoặc từ khóa được sử dụng để thực hiện các phép toán trên biến hoặc giá trị. Ví dụ, bạn muốn cộng hai số hoặc kiểm tra điều kiện? Toán tử sẽ giúp bạn làm điều đó.

    Trong C, toán tử được chia thành nhiều nhóm, từ toán tử số học đơn giản đến toán tử bitwise phức tạp hơn, rất hữu ích trong lập trình nhúng. Dưới đây chúng ta sẽ bàn về các toán tử trong C theo cách chia làm 6 nhóm.


Mục Lục


Toán Tử Số Học (Arithmetic Operators)

    Toán tử số học thực hiện các phép toán cơ bản như cộng, trừ, nhân, chia. Dưới đây là danh sách các toán tử số học:

Toán tử Mô tả Ví dụ Kết quả
+ Cộng hai giá trị 5 + 3 8
- Trừ hai giá trị 5 - 3 2
* Nhân hai giá trị 5 * 3 15
/ Chia hai giá trị 6 / 2 3
% Chia lấy nguyên (modulus) 7 % 2 1
++ Tăng giá trị lên 1 a = 5; ++a 6
-- Giảm giá trị xuống 1 a = 5; --a 4

    Toán tử tăng (++) giảm (--) có thể được sử dụng dưới dạng tiền tố (prefix) hoặc hậu tố (postfix), và chúng có sự khác biệt về cách hoạt động liên quan đến thứ tự thực hiện phép tăng/giảm và trả về giá trị.

  • Với Prefix: Giá trị của biến được tăng hoặc giảm trước, sau đó giá trị mới được sử dụng trong biểu thức hoặc trả về.
  • Với Postfix: Giá trị hiện tại của biến được sử dụng trong biểu thức hoặc trả về trước, sau đó biến được tăng hoặc giảm.
  1. #include <stdio.h>
  2. int main() {
  3. int x = 10, y;
  4. // Tiền tố
  5. y = ++x; // x tăng lên 11, y = 11
  6. printf("Prefix: x = %d, y = %d\n", x, y); // x = 11, y = 11
  7. // Hậu tố
  8. x = 10; // Reset x
  9. y = x++; // y = 10, x tăng lên 11
  10. printf("Postfix: x = %d, y = %d\n", x, y); // x = 11, y = 10
  11. // Sử dụng trong biểu thức
  12. x = 5;
  13. printf("Prefix in expression: %d\n", ++x * 2); // (6) * 2 = 12
  14. x = 5;
  15. printf("Postfix in expression: %d\n", x++ * 2); // (5) * 2 = 10, x = 6 sau đó
  16. return 0;
  17. }
  18.     Run This Code    

    Postfix - Tiền tố thường hiệu quả hơn vì không cần lưu trữ giá trị tạm, đặc biệt trong các vòng lặp hoặc hệ thống nhúng. Một lý do khác là hiệu ứng phụ mà Prefix gây ra trong các biểu thức phức tạp, vì giá trị được sử dụng trước khi thay đổi. Cần tránh lạm dụng trong cùng một biểu thức, ví dụ: a = a++ + ++a; là không xác định (undefined behavior).

Toán Tử Quan Hệ (Relational Operators)

    Toán tử quan hệ (Relational Operators) được sử dụng để so sánh hai giá trị hoặc biểu thức, trả về kết quả logic là true (1) hoặc false (0). Các toán tử này thường được dùng trong các câu lệnh điều kiện (if, while, for) để kiểm tra mối quan hệ giữa các giá trị.

    Toán tử quan hệ bao gồm các phép so sánh cơ bản:

Toán tử Mô tả Ví dụ Kết quả
== Kiểm tra bằng 5 == 5 1 (true)
!= Kiểm tra không bằng 5 != 3 1 (true)
> Kiểm tra lớn hơn 5 > 3 1 (true)
< Kiểm tra nhỏ hơn 5 < 3 0 (false)
>= Kiểm tra lớn hơn hoặc bằng 5 >= 5 1 (true)
<= Kiểm tra nhỏ hơn hoặc bằng 5 <= 3 0 (false)

    Ví dụ relational operators luôn trả về 0 hoặc 1, và sử dụng trong các câu lệnh điều kiện:

  1. #include <stdio.h>
  2. int main() {
  3. int a = 5, b = 3;
  4. printf("a == b: %d\n", a == b); // 0 (false)
  5. printf("a != b: %d\n", a != b); // 1 (true)
  6. printf("a > b: %d\n", a > b); // 1 (true)
  7. printf("a < b: %d\n", a < b); // 0 (false)
  8. printf("a >= b: %d\n", a >= b); // 1 (true)
  9. printf("a <= b: %d\n", a <= b); // 0 (false)
  10. // Sử dụng trong điều kiện
  11. if (a > b) {
  12. printf("%d lớn hơn %d\n", a, b); // In: 5 lớn hơn 3
  13. }
  14. return 0;
  15. }
  16.     Run This Code    

    ➤ Lưu ý quan trọng

    So sánh kiểu float: Khi so sánh số thực (float, double), cần cẩn thận do sai số làm tròn. Sử dụng khoảng chênh lệch nhỏ (epsilon) để so sánh gần đúng:

  1. #include <math.h>
  2. float a = 0.3, b = 0.1 + 0.2;
  3. if (fabs(a - b) < 0.0001) {
  4. printf("a gần bằng b\n");
  5. }

    ➥ Tính chính xác của Floating Point!

Toán Tử Logic (Logical Operators)

    Toán tử logic (Logical Operators) được sử dụng để kết hợp hoặc thao tác trên các biểu thức logic, trả về kết quả logic là true (1) hoặc false (0). Các toán tử này thường được dùng trong các câu lệnh điều kiện (if, while, for) để kiểm tra hoặc kết hợp các điều kiện.

    Toán tử logic trong C bao gồm ba loại chính:

Toán tử Mô tả Ví dụ Kết quả
&& Logic AND (trả về 1 nếu cả hai biểu thức là true) 1 && 0 0 (false)
|| Logic OR (trả về 1 nếu ít nhất một biểu thức là true) 1 || 0 1 (true)
! Logic NOT (đảo ngược giá trị logic) !1 0 (false)

    Ví dụ sử dụng logical operators:

  1. #include <stdio.h>
  2. int main() {
  3. int a = 1, b = 0, c = 5;
  4. // Logic AND
  5. printf("a && b: %d\n", a && b); // 0 (false, vì b = 0)
  6. printf("a && c: %d\n", a && c); // 1 (true, vì cả a và c khác 0)
  7. // Logic OR
  8. printf("a || b: %d\n", a || b); // 1 (true, vì a = 1)
  9. printf("b || c: %d\n", b || c); // 1 (true, vì c khác 0)
  10. // Logic NOT
  11. printf("!a: %d\n", !a); // 0 (false, vì a = 1)
  12. printf("!b: %d\n", !b); // 1 (true, vì b = 0)
  13. // Kết hợp với toán tử quan hệ
  14. if (a > 0 && c < 10) {
  15. printf("a > 0 và c < 10 là đúng\n"); // In câu này
  16. }
  17. return 0;
  18. }
  19.     Run This Code    

Toán Tử Bitwise

    Toán tử bitwise (Bitwise Operators) được sử dụng để thao tác trực tiếp trên các bit của giá trị nguyên (int, char, v.v.), cho phép thực hiện các phép toán logic trên từng bit riêng lẻ. Các toán tử này rất hữu ích trong lập trình hệ thống, hệ thống nhúng, hoặc các ứng dụng yêu cầu tối ưu hóa hiệu suất và thao tác ở mức thấp.

Toán tử bitwise bao gồm sáu loại chính, thao tác trên các bit của toán hạng

Toán tử Mô tả Ví dụ Kết quả (a = 5, b = 6)
& AND bit (1 nếu cả hai bit là 1) a & b 4 (101 & 110 = 100)
| OR bit (1 nếu ít nhất một bit là 1) a | b 7 (101 | 110 = 111)
^ XOR bit (1 nếu hai bit khác nhau) a ^ b 3 (101 ^ 110 = 011)
~ NOT bit (đảo tất cả bit) ~a -6 (~101 = 11111010)
<< Dịch trái (dịch bit sang trái, điền 0) a << 1 10 (101 << 1 = 1010)
>> Dịch phải (dịch bit sang phải) a >> 1 2 (101 >> 1 = 010)

    Toán tử bitwise chỉ áp dụng cho các kiểu nguyên (int, char, short, long, v.v.), không áp dụng cho float hoặc double.

    Bitwise là toán tử cực kỳ quan trọng và sử dụng thường xuyên trong Embeded, mình có một bài viết/video riêng về phần này tại đây:

    ➥ Toán tử Bitwise trong Embedded System!

Toán Tử Gán (Assignment Operators)

    Toán tử gán (Assignment Operators) được sử dụng để gán giá trị cho một biến hoặc thực hiện một phép toán kết hợp với gán. Toán tử gán cơ bản là = và các toán tử gán kết hợp khác (+=, -=, v.v.) giúp viết code ngắn gọn hơn.

    Toán tử gán trong C bao gồm toán tử gán đơn giản và các toán tử gán kết hợp với các phép toán số học hoặc bitwise:

Toán tử Mô tả Ví dụ Tương đương
= Gán giá trị a = b a = b
+= Cộng và gán a += b a = a + b
-= Trừ và gán a -= b a = a - b
*= Nhân và gán a *= b a = a * b
/= Chia và gán a /= b a = a / b
%= Chia lấy nguyên và gán a %= b a = a % b
&= AND bit và gán a &= b a = a & b
|= OR bit và gán a |= b a = a | b
^= XOR bit và gán a ^= b a = a ^ b
<<= Dịch trái và gán a <<= n a = a << n
>>= Dịch phải và gán a >>= n a = a >> n

    Phần này khá dễ sử dụng nên mình sẽ không cần lấy ví dụ!

Toán Tử Khác

    Trong C, ngoài các toán tử số học, quan hệ, logic, bitwise và gán, còn có một số toán tử khác (thường được gọi là toán tử đặc biệt hoặc toán tử điều kiện) phục vụ các mục đích cụ thể như kiểm tra kích thước, truy cập bộ nhớ, điều kiện ba ngôi, hoặc thực hiện các thao tác đặc thù.

Toán tử Mô tả Ví dụ Kết quả/Mô tả chi tiết
sizeof Trả về kích thước (byte) sizeof(int) 4 (trên hệ 32-bit)
& Lấy địa chỉ bộ nhớ &a Địa chỉ của biến a
* Giải tham chiếu con trỏ *ptr Giá trị tại địa chỉ ptr
?: Toán tử điều kiện ba ngôi a > b ? a : b Trả về a nếu a > b, ngược lại trả b
, Thực hiện tuần tự, trả về biểu thức cuối a, b Giá trị của b
. Truy cập thành viên cấu trúc p.x Giá trị thành viên x của struct p
Truy cập thành viên qua con trỏ ptrx Giá trị thành viên x của struct mà ptr trỏ đến

Toán tử sizeof

  • Trả về kích thước (tính bằng byte) của một biến, kiểu dữ liệu, hoặc biểu thức.
  • Kết quả của sizeof phụ thuộc vào nền tảng (ví dụ: int có thể là 2 hoặc 4 byte).
  • Ví dụ:

printf("Size of int: %zu\n", sizeof(int)); // 4 (trên hệ 32-bit)

Toán tử lấy địa chỉ (&)

  • Trả về địa chỉ bộ nhớ của một đối tượng (Biến, Hàm), toán tử này cực hữu ích và sẽ được bàn sâu hơn ở bài Pointer.
  • Ví dụ:

int a = 5;
printf("Address of a: %p\n", &a);
printf("Address of a: %p\n", &printf);
printf("Address of a: %p\n", &main);

Toán tử de-reference (*)

  • Truy cập giá trị tại địa chỉ mà con trỏ trỏ đến, có thể hiểu nó sẽ trái ngược với toán tử địa chỉ (&) ở trên, dùng để thao tác với con trỏ để đọc hoặc ghi giá trị.
  • Ví dụ:
int a = 5;
int *ptr = &a;
printf("Value at ptr: %d\n", *ptr); // 5

Toán tử điều kiện ba ngôi (?:)

  • Thay thế cho câu lệnh if-else đơn giản, có dạng condition ? expr1 : expr2.
    • Nếu condition là true, trả về expr1.
    • Nếu condition là false, trả về expr2.
  • Ứng dụng: Rút gọn mã khi gán giá trị hoặc trả về dựa trên điều kiện.
  • Ví dụ:

int a = 5, b = 10;
int max = (a > b) ? a : b;
printf("Max: %d\n", max); // 10

Toán tử dấu phẩy (,)

  • Thực hiện các biểu thức tuần tự từ trái sang phải, trả về giá trị của biểu thức cuối cùng.
  • Thường dùng trong vòng lặp hoặc khai báo nhiều biểu thức trong một dòng.
  • Ngoài ra thì không nên dùng cho mục đích khác vì code khó đọc!

Toán tử các phần tử của struct/union (.)

  • Truy cập phần tử của một biến kiểu struct hoặc union. Sẽ giới thiệu trong bài User-defined Data Type

Toán tử truy cập qua con trỏ ()

  • Truy cập thành viên của struct hoặc union thông qua con trỏ. Sẽ giới thiệu trong bài User-defined Data Type

Operator Precedence

    Chúng ta có rất / rất nhiều toán tử như kể trên, vậy khi viết vào cùng một biểu thức, chúng sẽ có thứ tự ưu tiên, giống như lúc học tiểu học chúng ta có quy tắc "nhân chia trước cộng trừ sau", thì trong lập trình nó cũng thế. Nhưng số lượng toán tử quá lớn và việc ghi nhớ toàn bộ thứ tự là gần như không thể - và cũng không cần thiết. Bạn nên ghi nhớ một số nguyên tắc đơn giản như:

  • Dùng dấu ngoặc để ưu tiên một phép toán - dấu ngoặc đơn () có ưu tiên cao nhất, nên cần cái gì làm trước thì cứ thêm ngoặc đơn vào cho chắc!
  • a = b * c + d
    "Nhân chia trước cộng trừ sau" thì rõ rồi, và tất cả các phép toán thì phép gán (assignment operator) sẽ thực hiện cuối cùng - tức là ưu tiên thấp nhất trong các phép toán.

    Đơn giản vậy thôi, còn dưới đây là cái bảng thứ tự ưu tiên!

Mức ưu tiên Toán tử Mô tả Tính kết hợp
1 (), [], ., ->, ++/-- (hậu tố) Dấu ngoặc, truy cập mảng/cấu trúc, tăng/giảm hậu tố Trái sang phải
2 ++/-- (tiền tố), +, -, !, ~, *, &, sizeof, (type) Tăng/giảm tiền tố, đơn ngôi, logic/bit NOT, con trỏ, địa chỉ, kích thước, ép kiểu Phải sang trái
3 *, /, % Nhân, chia, chia lấy nguyên Trái sang phải
4 +, - Cộng, trừ Trái sang phải
5 <<, >> Dịch trái, dịch phải Trái sang phải
6 <, <=, >, >= Quan hệ (nhỏ hơn, lớn hơn, v.v.) Trái sang phải
7 ==, != Bằng, không bằng Trái sang phải
8 & Bitwise AND Trái sang phải
9 ^ Bitwise XOR Trái sang phải
10 | Bitwise OR Trái sang phải
11 && Logic AND Trái sang phải
12 || Logic OR Trái sang phải
13 ?: Toán tử điều kiện ba ngôi Phải sang trái
14 =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= Gán và gán kết hợp Phải sang trái
15 , Toán tử dấu phẩy Trái sang phải

Kết Luận

    Video giới thiệu về các Operator trong C

    Trong lập trình Embedded C, nhìn chung là có rất nhiều toán tử và hoàn cảnh sử dụng cũng khác nhau. Đây là một kiến thức cơ bản mà sẽ được sử dụng thường xuyên ở những bài viết sau!

>>>>>> Follow ngay <<<<<<<

Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//