BlogTổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ
Bố cục bài viết
Trang này
Ngày tạo
07/09/2026
Cập nhật
Lượt xem
6
Đọc
10m
1. Công thức tính tổng số byte của một mảng
sizeof(arr) = số_phần_tử × sizeof(kiểu_dữ_liệu)
Ví dụ: uint32_t arr[4] → 4 × sizeof(uint32_t) = 4 × 4 = 16 byte.
Lưu ý quan trọng: mọi phần tử trong mảng đều cùng một kiểu dữ liệu, nên chiếm số byte bằng nhau. Chỉ số n trong arr[n] không quyết định “phần tử to hay nhỏ” — nó chỉ quyết định vị trí (offset) của phần tử đó.
2. Công thức tính vị trí (offset) của từng phần tử
offset_byte(arr[n]) = n × sizeof(kiểu_dữ_liệu)
Ví dụ với arr bắt đầu ở địa chỉ 0x1000, kiểu uint32_t (4 byte/phần tử):
| Phần tử | offset | Địa chỉ bắt đầu | Byte chiếm dụng |
|---|---|---|---|
arr[0] | 0 × 4 = 0 | 0x1000 | byte 0–3 |
arr[1] | 1 × 4 = 4 | 0x1004 | byte 4–7 |
arr[2] | 2 × 4 = 8 | 0x1008 | byte 8–11 |
arr[3] | 3 × 4 = 12 | 0x100C | byte 12–15 |
Offset là gì, hiểu cho kỹ
Offset = “độ lệch” tính từ một điểm mốc, đo bằng byte. Ví dụ: đứng ở đầu một dãy nhà (mốc = nhà đầu tiên), muốn tới nhà thứ 3, bạn chỉ cần biết “đi thêm 2 căn” — con số đó chính là offset.
địa_chỉ_thật = địa_chỉ_mốc + offset
Offset không phải là địa chỉ tuyệt đối — nó là khoảng cách tương đối. Khái niệm này cũng dùng để giải thích vị trí từng field trong struct (mỗi field có offset riêng tính từ đầu struct), hoặc vị trí dữ liệu trong file/gói tin nhị phân.
0x1000 không phải là mốc cố định của C/C++ — đó chỉ là con số ví dụ cho dễ tính nhẩm. Địa chỉ thật của một biến do hệ điều hành cấp phát lúc chương trình chạy (runtime), thường ngẫu nhiên hoá bởi ASLR, và khác nhau mỗi lần chạy. Muốn biết địa chỉ thật, phải in ra bằng printf("%p", (void*)&arr).
3. Quy tắc pointer arithmetic (cộng con trỏ)
ptr + n = địa_chỉ(ptr) + n × sizeof(kiểu_dữ_liệu_ptr_trỏ_tới)
Con trỏ luôn cộng theo số phần tử, không phải theo byte một cách “ngây thơ”. Trình biên dịch tự nhân với sizeof(kiểu dữ liệu) để ra số byte thực sự cần nhảy.
So sánh hai kiểu con trỏ cùng trỏ vào arr
uint32_t arr[4];
uint32_t *p = arr; // p trỏ theo kiểu uint32_t (4 byte/bước)
uint8_t *b = (uint8_t*)arr; // b trỏ theo kiểu uint8_t (1 byte/bước)
| Biểu thức | Loại | Số byte nhảy | Kết quả |
|---|---|---|---|
p + 1 | uint32_t* | 4 byte | trỏ đúng vào arr[1] (0x1004) |
b + 1 | uint8_t* | 1 byte | trỏ vào byte thứ 2 bên trong arr[0] (0x1001), không phải arr[1] |
Kết luận: chỉ khi kiểu con trỏ khớp với kiểu phần tử của mảng thì ptr + n mới tương đương arr[n]. Ép về uint8_t* là “tháo rời” mảng thành từng byte riêng lẻ.
Phân biệt con trỏ và giá trị tại con trỏ
b + 1→ là một địa chỉ (vị trí bộ nhớ), không phải một con số giá trị.*(b + 1)→ mới là giá trị byte thực sự nằm tại địa chỉ đó (phải dùng dấu*để dereference).
uint32_t *p = arr; có tương đương &arr[0] không?
Có, hoàn toàn tương đương:
uint32_t *p1 = arr; // decay tự động
uint32_t *p2 = &arr[0]; // lấy địa chỉ phần tử 0 tường minh
Đây là quy tắc array decay: khi tên mảng (arr) xuất hiện trong hầu hết biểu thức, nó tự động “phân rã” thành con trỏ trỏ tới phần tử đầu tiên.
Hai ngoại lệ không decay, cần nhớ để tránh nhầm:
sizeof(arr) // = 16 (toàn bộ mảng), KHÔNG phải sizeof(con trỏ)
&arr // kiểu uint32_t(*)[4] — con trỏ trỏ tới CẢ MẢNG, khác kiểu &arr[0]
arr, &arr[0], &arr cùng ra một địa chỉ số, nhưng &arr + 1 sẽ nhảy nguyên 16 byte (hết mảng), khác hẳn p + 1 chỉ nhảy 4 byte.
4. Cách một số nguyên được tách thành các byte (Endianness)
Một số uint32_t (32 bit) được tách thành 4 mảnh 8-bit (4 byte), mỗi byte ứng với một khoảng vị trí bit:
byte3 byte2 byte1 byte0
bit 24–31 bit 16–23 bit 8–15 bit 0–7
Công thức ghép byte thành số nguyên (little-endian)
giá_trị = byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24)
Little-endian là gì
Quy ước lưu byte thấp nhất (byte0, ứng với bit 0–7) vào địa chỉ bộ nhớ nhỏ nhất, rồi tăng dần địa chỉ thì lưu byte cao dần. Đây là quy ước phổ biến trên x86, ARM (mặc định). Big-endian làm ngược lại — byte cao nhất nằm ở địa chỉ thấp nhất.
Vì sao gọi là byte “thấp nhất” — cách nhận biết
“Thấp/cao” ở đây nói về trọng số toán học của byte trong giá trị số, giống hệt khái niệm hàng đơn vị/hàng nghìn trong số thập phân. Ví dụ số 5678 = 5×1000 + 6×100 + 7×10 + 8×1 — chữ số 8 ít quan trọng nhất (ít ảnh hưởng tới tổng giá trị), chữ số 5 quan trọng nhất.
Áp dụng cho 0x12345678:
0x12345678 = (0x12 << 24) | (0x34 << 16) | (0x56 << 8) | (0x78 << 0)
| Byte | Dịch bit | Đóng góp vào giá trị | Vai trò |
|---|---|---|---|
0x78 | << 0 | 0x78 × 1 | thấp nhất |
0x56 | << 8 | 0x56 × 256 | trung bình thấp |
0x34 | << 16 | 0x34 × 65536 | trung bình cao |
0x12 | << 24 | 0x12 × 16,777,216 | cao nhất |
Quy tắc đọc nhanh: khi viết số hex theo cách thông thường (trái sang phải), cặp chữ số hex bên phải cùng luôn là byte thấp nhất, cặp bên trái cùng luôn là byte cao nhất — bất kể endianness. Endianness chỉ quyết định byte đó được lưu vào địa chỉ bộ nhớ nào, không đổi trọng số toán học của nó.
5. Ví dụ minh họa
Ví dụ 1: arr[0] = 0x12345678
| Địa chỉ | Tên | Giá trị byte |
|---|---|---|
0x1000 | b[0] | 0x78 |
0x1001 | b[1] | 0x56 |
0x1002 | b[2] | 0x34 |
0x1003 | b[3] | 0x12 |
Kiểm tra ghép lại: 0x78 | (0x56<<8) | (0x34<<16) | (0x12<<24) = 0x12345678 ✅
Ví dụ 2: arr[0] = 100 (tức 0x00000064)
| Địa chỉ | Tên | Giá trị byte |
|---|---|---|
0x1000 | b[0] | 0x64 |
0x1001 | b[1] | 0x00 |
0x1002 | b[2] | 0x00 |
0x1003 | b[3] | 0x00 |
→ *(b + 1) = 0x00 = 0, vì số 100 nhỏ nên chỉ byte thấp nhất mang giá trị thật, 3 byte còn lại đều bằng 0.
6. Hex, thập phân chỉ là cách viết — công thức không đổi
0x1000 và 4096 là cùng một con số, chỉ khác hệ đếm biểu diễn. Mọi công thức (offset, pointer arithmetic, tách byte) áp dụng y hệt bất kể bạn dùng hex hay thập phân:
| Dùng hex | Dùng thập phân | |
|---|---|---|
Địa chỉ arr | 0x1000 | 4096 |
offset của arr[1] | 4 byte | 4 byte |
Địa chỉ arr[1] | 0x1004 | 4100 |
Lập trình viên hay dùng hex khi nói về địa chỉ/byte không phải vì “chính xác hơn”, mà vì hex map gọn với nhị phân: mỗi chữ số hex = đúng 4 bit, nên 1 byte = đúng 2 chữ số hex.
Lưu ý: sizeof và offset luôn là số nguyên, không bao giờ có giá trị như 40.5 — vì byte là đơn vị nhỏ nhất, không thể chia nhỏ hơn mà bộ nhớ có thể đánh địa chỉ. Ngay cả bit-field (field chỉ chiếm vài bit trong struct) cũng không thể lấy địa chỉ riêng (&field) vì phần cứng chỉ đánh địa chỉ theo byte.
7. Số thực (float) được lưu trữ khác hẳn số nguyên
Số nguyên lưu trực tiếp giá trị dưới dạng nhị phân, nhưng số thực (float, double) cần biểu diễn cả số rất lớn lẫn rất nhỏ trong cùng số byte cố định. Chuẩn IEEE 754 chia 32 bit của float thành 3 vùng:
| Vùng | Số bit | Ý nghĩa |
|---|---|---|
| Dấu (sign) | 1 bit | 0 = dương, 1 = âm |
| Số mũ (exponent) | 8 bit | lưu dạng bias: số_mũ_thật + 127 |
| Phần định trị (mantissa) | 23 bit | phần lẻ sau khi chuẩn hóa dạng 1.xxxxx |
Công thức:
giá_trị = (-1)^dấu × 1.phần_định_trị × 2^(số_mũ − 127)
Ví dụ: số 1.0
| Vùng | Giá trị lưu | Ý nghĩa |
|---|---|---|
| Dấu | 0 | dương |
| Số mũ | 01111111 (=127) | số mũ thật = 127 − 127 = 0 |
| Mantissa | toàn số 0 | phần lẻ = 0 |
Ghép lại: 1.0 × 2^0 = 1.0 ✅
Vì sao khác số nguyên: với số nguyên, mỗi bit mang trọng số cố định (bit 0 = 1, bit 1 = 2…), nên pointer arithmetic và tách byte hoạt động trực quan. Với float, các bit mã hóa dấu/số mũ/mantissa theo công thức riêng — vẫn tháo rời thành byte được (vì float cũng chỉ là 4 byte trong bộ nhớ), nhưng diễn giải từng bit như số nguyên thông thường sẽ sai hoàn toàn. double (8 byte) dùng cùng nguyên tắc với tỷ lệ khác: 1 bit dấu, 11 bit số mũ, 52 bit mantissa.
8. Ghi nhớ nhanh
- Kích thước mảng = số phần tử ×
sizeof(kiểu). - Offset của
arr[n]=n × sizeof(kiểu)— là khoảng cách tương đối tính từ một địa chỉ mốc, không phải địa chỉ tuyệt đối. ptr + nluôn nhảy theo kiểu dữ liệu của ptr, không phải theonđơn thuần.ptr + nlà địa chỉ;*(ptr + n)mới là giá trị.uint32_t *p = arr;tương đương&arr[0]nhờ array decay — nhưng&arrlà kiểu khác, trỏ tới cả mảng.- Ép
uint8_t*để duyệt bộ nhớ theo từng byte thô, bất kể ranh giới phần tử gốc. - Thứ tự byte trong bộ nhớ phụ thuộc endianness của máy — dùng code kiểm tra thực tế thay vì giả định.
sizeof/offset luôn là số nguyên; không có khái niệm “nửa byte”.- Số thực (
float/double) lưu theo chuẩn IEEE 754 (dấu, số mũ, mantissa), không thể diễn giải như số nguyên thông thường dù cùng nằm trong byte.
#include <stdio.h>
#include <stdint.h>
int main() {
uint32_t arr[4] = {0x12345678, 0, 0, 0};
uint8_t *b = (uint8_t*)arr;
for (int i = 0; i < 4; i++)
printf("byte %d = 0x%02X\n", i, b[i]);
return 0;
}
Lộ trình học tập
Lập Trình C/C++
- 1 01 — C Cơ Bản
- 2 02 — Con Trỏ & Bộ Nhớ
- 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
- 4 04 — C Nâng Cao
- 5 05 — C++ Cốt Lõi
- 6 06 — C++ Hiện Đại
- 7 07 — Lập Trình Hệ Thống & Mạng
- 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
- 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
- 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
- 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
- 12 Tổng hợp các dạng printf để in giá trị trong C
- 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
- 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ