This C programming guide is currently only available in Vietnamese. Please toggle the language switch (🇻🇳) in the top navigation to read the full article.
In this third part of the series, we cover arithmetic, comparison, and logical operators, the operator precedence table, bitwise operations (AND, OR, XOR, NOT, shifts), endianness, and real-world bitwise applications including RGB color packing and IP address manipulation.
Trong bài học này, chúng ta sẽ tìm hiểu về các loại toán tử trong C, bảng thứ tự ưu tiên toán tử, và đặc biệt là các phép toán Bitwise (xử lý bit mức thấp) cùng những ứng dụng thực tế cực kỳ mạnh mẽ.
1. Toán tử số học & so sánh
C cung cấp đầy đủ các toán tử cơ bản để thực hiện phép tính và so sánh giá trị.
A. Toán tử số học (Arithmetic Operators)
| Toán tử | Ý nghĩa | Ví dụ | Kết quả |
|---|---|---|---|
| + | Cộng | 5 + 3 |
8 |
| - | Trừ | 5 - 3 |
2 |
| * | Nhân | 5 * 3 |
15 |
| / | Chia | 7 / 2 |
3 (chia nguyên) |
| % | Chia lấy dư (Modulo) | 7 % 2 |
1 |
| ++ | Tăng 1 | a++ hoặc ++a |
a = a + 1 |
| -- | Giảm 1 | a-- hoặc --a |
a = a - 1 |
B. Toán tử so sánh (Comparison Operators)
Các toán tử so sánh trả về 1 (đúng/true) hoặc 0 (sai/false):
| Toán tử | Ý nghĩa | Ví dụ (a=5, b=3) | Kết quả |
|---|---|---|---|
| == | Bằng | a == b |
0 |
| != | Khác | a != b |
1 |
| > | Lớn hơn | a > b |
1 |
| < | Nhỏ hơn | a < b |
0 |
| >= | Lớn hơn hoặc bằng | a >= 5 |
1 |
| <= | Nhỏ hơn hoặc bằng | a <= 3 |
0 |
C. Toán tử logic (Logical Operators)
| Toán tử | Ý nghĩa | Ví dụ | Kết quả |
|---|---|---|---|
| && | AND logic | (5 > 3) && (2 < 4) |
1 (cả hai đúng) |
| || | OR logic | (5 > 3) || (2 > 4) |
1 (ít nhất một đúng) |
| ! | NOT logic | !(5 > 3) |
0 (phủ định đúng = sai) |
#include <stdio.h>
int main() {
int a = 10, b = 3;
// Toán tử số học
printf("a + b = %d\n", a + b); // 13
printf("a - b = %d\n", a - b); // 7
printf("a * b = %d\n", a * b); // 30
printf("a / b = %d\n", a / b); // 3 (chia nguyên, bỏ phần thập phân)
printf("a %% b = %d\n", a % b); // 1 (phần dư)
// ⚠️ Lưu ý: ++a vs a++
int x = 5;
printf("++x = %d\n", ++x); // 6 (tăng TRƯỚC, rồi dùng giá trị)
printf("x++ = %d\n", x++); // 6 (dùng giá trị TRƯỚC, rồi tăng)
printf("x = %d\n", x); // 7 (đã tăng sau ở dòng trên)
// Toán tử so sánh & logic
int age = 20;
int hasID = 1; // 1 = true
if (age >= 18 && hasID) {
printf("Du dieu kien vao cua\n");
}
return 0;
}
2. Bảng thứ tự ưu tiên toán tử (Operator Precedence)
Thứ tự ưu tiên quyết định toán tử nào được thực hiện trước trong một biểu thức phức tạp. Toán tử ở hàng trên có độ ưu tiên cao hơn. Tham khảo đầy đủ tại cppreference.com - Operator Precedence.
| Ưu tiên | Toán tử | Mô tả | Kết hợp |
|---|---|---|---|
| 1 (cao nhất) | () [] -> . | Gọi hàm, chỉ số mảng, truy cập thành viên | Trái → Phải |
| 2 | ++ -- + - ! ~ * & (type) sizeof | Toán tử một ngôi (unary), ép kiểu, sizeof | Phải → Trái |
| 3 | * / % | Nhân, chia, chia lấy dư | Trái → Phải |
| 4 | + - | Cộng, trừ | Trái → Phải |
| 5 | << >> | Dịch bit trái, dịch bit phải | Trái → Phải |
| 6 | < <= > >= | So sánh lớn nhỏ | Trái → Phải |
| 7 | == != | So sánh bằng, khác | Trái → Phải |
| 8 | & | AND bitwise | Trái → Phải |
| 9 | ^ | XOR bitwise | Trái → Phải |
| 10 | | | OR bitwise | Trái → Phải |
| 11 | && | AND logic | Trái → Phải |
| 12 | || | OR logic | Trái → Phải |
| 13 | ?: | Toán tử điều kiện (ternary) | Phải → Trái |
| 14 | = += -= *= /= %= <<= >>= &= ^= |= | Gán và gán kết hợp | Phải → Trái |
| 15 (thấp nhất) | , | Toán tử phẩy (comma) | Trái → Phải |
Lỗi thường gặp do nhầm lẫn thứ tự ưu tiên:
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30};
int *p = arr;
// Bẫy 1: *p++ vs (*p)++
// *p++ → *(p++) → lấy giá trị tại p, rồi tăng CON TRỎ p lên 1
// (*p)++ → lấy giá trị tại p, rồi tăng GIÁ TRỊ đó lên 1
printf("*p = %d\n", *p); // 10
printf("*p++ = %d\n", *p++); // 10, sau đó p trỏ tới arr[1]
printf("*p = %d\n", *p); // 20 (p đã dịch chuyển)
// Bẫy 2: & có ưu tiên THẤP hơn ==
int x = 5, y = 3;
// Sai: if (x & 1 == 0) → thực ra là: if (x & (1 == 0)) → if (x & 0) → 0
// Đúng: if ((x & 1) == 0)
if ((x & 1) == 0) {
printf("%d la so chan\n", x);
} else {
printf("%d la so le\n", x); // Kết quả đúng
}
// Bẫy 3: Toán tử gán = vs so sánh ==
int n = 0;
// if (n = 5) luôn đúng vì gán n = 5, rồi kiểm tra 5 != 0 → true
// Ý đúng: if (n == 5)
if (n == 5) {
printf("n bang 5\n");
} else {
printf("n khong bang 5\n"); // Kết quả đúng
}
return 0;
}
3. Các phép toán Bitwise & Endianness
Máy tính lưu trữ mọi dữ liệu dưới dạng các dãy bit nhị phân (chỉ gồm các số 0 và
1). Phép toán Bitwise cho phép chúng ta can thiệp và xử lý trực tiếp trên từng bit nhị
phân của số nguyên. Đây là công cụ cực kỳ mạnh mẽ để tối ưu hóa hiệu năng phần cứng.
A. Khái niệm về Endianness (Byte Ordering)
Khi lưu trữ một kiểu dữ liệu nhiều bytes (như số nguyên 4 bytes) xuống bộ nhớ RAM, các byte đó sẽ được sắp xếp theo thứ tự nào? C chia làm hai chuẩn chính:
- Little Endian: Byte có trọng số thấp nhất (Least Significant Byte - LSB) được lưu trữ tại địa chỉ nhớ thấp nhất. Hầu hết các CPU hiện đại của Intel/AMD và cấu trúc ARM đều sử dụng Little Endian.
- Big Endian: Byte có trọng số cao nhất (Most Significant Byte - MSB) được lưu trữ tại địa chỉ thấp nhất (giống cách viết số thông thường từ trái qua phải). Thường gặp trong các thiết bị mạng hoặc dòng vi xử lý cũ.
Ta có thể viết một đoạn mã C ngắn gọn để kiểm tra hệ thống hiện tại đang chạy theo chuẩn nào bằng cách sử dụng ép kiểu con trỏ nhị phân:
#include <stdio.h>
int main() {
unsigned int x = 1; // Nhị phân: 0x00000001
// Ép kiểu con trỏ thành char* để đọc byte đầu tiên tại địa chỉ thấp nhất
char *c = (char*)&x;
if (*c == 1) {
printf("Hệ thống là Little Endian\n");
} else {
printf("Hệ thống là Big Endian\n");
}
return 0;
}
B. Các toán tử Bitwise thông dụng:
| Toán tử | Ý nghĩa | Quy tắc hoạt động |
|---|---|---|
| & | AND bitwise | Trả về 1 nếu cả hai bit đều là 1. Ngược lại là 0. |
| | | OR bitwise | Trả về 1 nếu ít nhất một trong hai bit là 1. |
| ^ | XOR bitwise | Trả về 1 nếu hai bit khác nhau. Trả về 0 nếu giống nhau. |
| ~ | NOT (Phủ định) | Đảo ngược bit: 0 thành 1, 1 thành 0. |
| << | Dịch trái (Shift Left) | Dịch các bit sang trái n vị trí (Tương đương nhân với 2^n). |
| >> | Dịch phải (Shift Right) | Dịch các bit sang phải n vị trí (Tương đương chia lấy nguyên cho 2^n). |
C. Các thủ thuật xử lý Bit siêu tốc (Bitwise Tricks)
-
Kiểm tra chẵn lẻ cực nhanh: Dùng
(n & 1). Phép toán này kiểm tra bit cuối cùng của số nguyên. Nếu bằng 0 thì số chẵn, bằng 1 thì số lẻ, tốc độ nhanh hơn nhiều so với phép chia lấy dưn % 2vì không cần chạy mạch chia logic của ALU. -
Bit-Packing (Gộp nhiều tùy chọn): Dùng mặt nạ bit (bitmask) để lưu trữ nhiều tùy
chọn bật/tắt (Boolean) vào một biến duy nhất để tiết kiệm RAM.
#define READ_PERMISSION (1 << 0) // 0001 #define WRITE_PERMISSION (1 << 1) // 0010 #define EXEC_PERMISSION (1 << 2) // 0100 int user_flag = READ_PERMISSION | WRITE_PERMISSION; // 0011 (Cho phép đọc và ghi) -
Xóa bit 1 thấp nhất (Lowest Set Bit): Công thức
n & (n - 1). Đây là thuật toán Brian Kernighan cực kỳ nổi tiếng dùng để đếm số lượng bit 1 có trong một số nguyên hoặc kiểm tra xem một số có phải là lũy thừa của 2 hay không (nếu kết quả bằng 0).
Ví dụ minh họa tổng hợp:
#include <stdio.h>
int main() {
int a = 5; // Nhị phân: 0101
int b = 9; // Nhị phân: 1001
printf("a & b = %d\n", a & b); // Ket qua: 1
printf("a | b = %d\n", a | b); // Ket qua: 13
printf("a ^ b = %d\n", a ^ b); // Ket qua: 12
printf("~a = %d\n", ~a); // Ket qua: -6 (theo dang bu hai)
printf("a << 1 = %d\n", a << 1); // Ket qua: 10 (tương đương 5 * 2)
printf("b >> 1 = %d\n", b >> 1); // Ket qua: 4 (tương đương 9 / 2)
// Meo kiem tra so chan/le sieu nhanh bang Bitwise
int n = 7;
if ((n & 1) == 0) {
printf("%d la so chan\n", n);
} else {
printf("%d la so le\n", n); // 7 & 1 = 0111 & 0001 = 0001 (khac 0) -> So le
}
return 0;
}
4. Ứng dụng Bitwise trong thực tế
Phép toán bitwise không chỉ là lý thuyết hàn lâm mà được ứng dụng rộng rãi trong thực tế, từ xử lý đồ họa, mạng máy tính đến lập trình nhúng.
A. RGB Color Packing - Gộp 3 byte màu vào 1 số nguyên
Trong đồ họa máy tính, mỗi pixel màu được biểu diễn bằng 3 thành phần Red, Green, Blue (mỗi thành phần 1 byte = 0-255). Thay vì dùng 3 biến riêng biệt, ta có thể gộp cả 3 vào một số nguyên 32-bit duy nhất bằng phép dịch bit:
#include <stdio.h>
#include <stdint.h>
// Gộp 3 byte R, G, B thành 1 số nguyên 32-bit: 0x00RRGGBB
uint32_t rgb_pack(uint8_t r, uint8_t g, uint8_t b) {
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
// Tách lại từng thành phần màu từ số nguyên đã gộp
void rgb_unpack(uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b) {
*r = (color >> 16) & 0xFF; // Dịch phải 16 bit, lấy 8 bit thấp
*g = (color >> 8) & 0xFF; // Dịch phải 8 bit, lấy 8 bit thấp
*b = color & 0xFF; // Lấy 8 bit thấp nhất
}
int main() {
// Màu cam: R=255, G=165, B=0
uint32_t orange = rgb_pack(255, 165, 0);
printf("Mau cam packed: 0x%06X\n", orange); // 0xFFA500
// Tách lại
uint8_t r, g, b;
rgb_unpack(orange, &r, &g, &b);
printf("R=%d, G=%d, B=%d\n", r, g, b); // R=255, G=165, B=0
// Pha trộn hai màu (trung bình từng kênh)
uint32_t white = rgb_pack(255, 255, 255);
uint8_t r2, g2, b2;
rgb_unpack(white, &r2, &g2, &b2);
uint32_t blended = rgb_pack((r + r2) / 2, (g + g2) / 2, (b + b2) / 2);
printf("Pha tron: 0x%06X\n", blended); // Màu cam nhạt
return 0;
}
B. IP Address Manipulation - Xử lý địa chỉ IP bằng Bitwise
Địa chỉ IPv4 (ví dụ: 192.168.1.100) thực chất là một số nguyên 32-bit, chia thành 4 octet
(mỗi octet 8 bit). Ta có thể dùng bitwise để gộp, tách và kiểm tra subnet:
#include <stdio.h>
#include <stdint.h>
// Gộp 4 octet thành địa chỉ IP 32-bit
uint32_t ip_pack(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return ((uint32_t)a << 24) | ((uint32_t)b << 16)
| ((uint32_t)c << 8) | d;
}
// In địa chỉ IP dạng dotted-decimal
void ip_print(uint32_t ip) {
printf("%d.%d.%d.%d",
(ip >> 24) & 0xFF,
(ip >> 16) & 0xFF,
(ip >> 8) & 0xFF,
ip & 0xFF);
}
int main() {
uint32_t ip = ip_pack(192, 168, 1, 100);
uint32_t mask = ip_pack(255, 255, 255, 0); // /24 subnet mask
// Tính network address bằng AND
uint32_t network = ip & mask;
// Tính broadcast address bằng OR với NOT mask
uint32_t broadcast = ip | ~mask;
printf("IP: "); ip_print(ip); printf("\n");
printf("Mask: "); ip_print(mask); printf("\n");
printf("Network: "); ip_print(network); printf("\n"); // 192.168.1.0
printf("Broadcast: "); ip_print(broadcast); printf("\n"); // 192.168.1.255
// Kiểm tra 2 IP có cùng subnet không
uint32_t ip2 = ip_pack(192, 168, 1, 200);
if ((ip & mask) == (ip2 & mask)) {
printf("Hai IP cung subnet!\n");
}
return 0;
}
C. Flag Registers - Cờ trạng thái bằng Bitmask
Trong lập trình nhúng và hệ thống, các thanh ghi (register) thường dùng từng bit riêng lẻ để biểu thị các trạng thái khác nhau. Dùng bitwise để bật, tắt, kiểm tra và toggle (đảo) từng cờ:
#include <stdio.h>
#include <stdint.h>
// Định nghĩa các cờ trạng thái (mỗi cờ chiếm 1 bit)
#define FLAG_ACTIVE (1 << 0) // Bit 0: Đang hoạt động
#define FLAG_ADMIN (1 << 1) // Bit 1: Quyền admin
#define FLAG_VERIFIED (1 << 2) // Bit 2: Đã xác minh
#define FLAG_BANNED (1 << 3) // Bit 3: Bị cấm
void print_flags(uint8_t flags) {
printf("Flags: [%s%s%s%s] (0b",
(flags & FLAG_ACTIVE) ? "ACTIVE " : "",
(flags & FLAG_ADMIN) ? "ADMIN " : "",
(flags & FLAG_VERIFIED) ? "VERIFIED " : "",
(flags & FLAG_BANNED) ? "BANNED " : "");
// In dạng nhị phân 8-bit
for (int i = 7; i >= 0; i--) {
printf("%d", (flags >> i) & 1);
}
printf(")\n");
}
int main() {
uint8_t user_flags = 0; // Khởi tạo: không có cờ nào
// BẬT cờ: dùng OR (|)
user_flags |= FLAG_ACTIVE; // Bật bit 0
user_flags |= FLAG_VERIFIED; // Bật bit 2
print_flags(user_flags); // [ACTIVE VERIFIED ] (0b00000101)
// TẮT cờ: dùng AND với NOT (~)
user_flags &= ~FLAG_VERIFIED; // Tắt bit 2
print_flags(user_flags); // [ACTIVE ] (0b00000001)
// TOGGLE (đảo) cờ: dùng XOR (^)
user_flags ^= FLAG_ADMIN; // Bật admin (vì đang tắt)
print_flags(user_flags); // [ACTIVE ADMIN ] (0b00000011)
user_flags ^= FLAG_ADMIN; // Tắt admin (vì đang bật)
print_flags(user_flags); // [ACTIVE ] (0b00000001)
// KIỂM TRA cờ: dùng AND (&)
if (user_flags & FLAG_ACTIVE) {
printf("User dang hoat dong\n");
}
if (!(user_flags & FLAG_BANNED)) {
printf("User chua bi cam\n");
}
return 0;
}
📥 Tải về mã nguồn mẫu: bitwise.c
Bạn có biết?
Phép toán n & 1 dùng để kiểm tra tính chẵn lẻ của một số nhanh hơn rất nhiều so với
phép chia lấy dư n % 2 thông thường, vì nó kiểm tra trực tiếp bit cuối cùng của số đó
(bit cuối là 1 thì số lẻ, là 0 thì số chẵn).
int a = 5; (nhị phân: 0101) và int b = 9; (nhị
phân: 1001). Kết quả của phép toán Bitwise XOR a ^ b ở hệ thập phân là bao
nhiêu?
Comments
Bình luận