Skip to content
LIVE FEED

BlogTổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ

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 đầuByte chiếm dụng
arr[0]0 × 4 = 00x1000byte 0–3
arr[1]1 × 4 = 40x1004byte 4–7
arr[2]2 × 4 = 80x1008byte 8–11
arr[3]3 × 4 = 120x100Cbyte 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ứcLoạiSố byte nhảyKết quả
p + 1uint32_t*4 bytetrỏ đúng vào arr[1] (0x1004)
b + 1uint8_t*1 bytetrỏ 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)
ByteDịch bitĐóng góp vào giá trịVai trò
0x78<< 00x78 × 1thấp nhất
0x56<< 80x56 × 256trung bình thấp
0x34<< 160x34 × 65536trung bình cao
0x12<< 240x12 × 16,777,216cao 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ênGiá trị byte
0x1000b[0]0x78
0x1001b[1]0x56
0x1002b[2]0x34
0x1003b[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ênGiá trị byte
0x1000b[0]0x64
0x1001b[1]0x00
0x1002b[2]0x00
0x1003b[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 hexDùng thập phân
Địa chỉ arr0x10004096
offset của arr[1]4 byte4 byte
Địa chỉ arr[1]0x10044100

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ùngSố bitÝ nghĩa
Dấu (sign)1 bit0 = dương, 1 = âm
Số mũ (exponent)8 bitlưu dạng bias: số_mũ_thật + 127
Phần định trị (mantissa)23 bitphầ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ùngGiá trị lưuÝ nghĩa
Dấu0dương
Số mũ01111111 (=127)số mũ thật = 127 − 127 = 0
Mantissatoàn số 0phầ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 + n luôn nhảy theo kiểu dữ liệu của ptr, không phải theo n đơn thuần.
  • ptr + n là địa chỉ; *(ptr + n) mới là giá trị.
  • uint32_t *p = arr; tương đương &arr[0] nhờ array decay — nhưng &arr là 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. 1 01 — C Cơ Bản
  2. 2 02 — Con Trỏ & Bộ Nhớ
  3. 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
  4. 4 04 — C Nâng Cao
  5. 5 05 — C++ Cốt Lõi
  6. 6 06 — C++ Hiện Đại
  7. 7 07 — Lập Trình Hệ Thống & Mạng
  8. 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
  9. 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
  10. 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
  11. 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
  12. 12 Tổng hợp các dạng printf để in giá trị trong C
  13. 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
  14. 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ
Contact
Facebook
TOP