Kỹ Thuật Thao Tác Bộ Nhớ Trực Tiếp Trong C

Giới thiệu về thao tác khối dữ liệu

Các hàm xử lý chuỗi tiêu chuẩn như strcpy hay strncpy có giới hạn lớn khi làm việc với dữ liệu không phải là chuỗi ký tự. Chúng dừng lại khi gặp ký tự null ('\0'). Để giải quyết nhu cầu sao chép các mảng số nguyên, cấu trúc dữ liệu phức tạp hay bất kỳ khối dữ liệu thô nào, chúng ta cần sử dụng nhóm hàm quản lý bộ nhớ cấp thấp.

Hàm sao chép bộ nhớ memcpy

Định nghĩa và cơ chế

Hàm này thực hiện chuyển dịch một khối dữ liệu cụ thể từ vùng nguồn sang vùng đích dựa trên kích thước byte quy định:

void * memcpy(void * dest, const void * src, size_t count);
  • Bắt đầu sao chép từ địa chỉ src đến dest tổng cộng count byte.
  • Toàn bộ quá trình diễn ra theo byte, không quan tâm đến ký tự null nằm bên trong dữ liệu.
  • Không đảm bảo an toàn nếu hai vùng bộ nhớ có phần giao thoa (overlap).
  • Cần khai báo thư viện #include <string.h>.

Ví dụ minh họa thực tế

Thay vì dùng chuỗi ký tự, hãy xem xét việc di chuyển mảng số nguyên. Kích thước mỗi phần tử int thường là 4 byte, do đó cần tính toán chính xác tổng byte cần chuyển.

#include <stdio.h>
#include <string.h>

int main() {
    int source_data[] = { 10, 20, 30, 40, 50 };
    int target_buffer[10] = { 0 };
    
    // Tính số byte: 5 phần tử * 4 byte/phần tử = 20 byte
    memcpy(target_buffer, source_data, sizeof(source_data));
    
    for (int i = 0; i < 5; i++) {
        printf("%d ", target_buffer[i]);
    }
    return 0;
}

Điều này chứng minh rằng hàm này chỉ đếm số lượng byte mà không phân biệt kiểu dữ liệu chứa bên trong.

Tự triển khai thuật toán

Nguyên lý cơ bản là lặp qua từng byte và gán giá trị. Dưới đây là phiên bản tùy chỉnh với tên biến khác biệt để làm rõ logic:

void* custom_memcpy(void* ptr_dest, const void* ptr_src, size_t size_bytes) {
    void* original_ptr = ptr_dest;
    
    if (!ptr_dest || !ptr_src) return NULL;

    while (size_bytes--) {
        // Ép ngầm định thành unsigned char để truy cập byte đơn lẻ
        *((unsigned char*)ptr_dest) = *((unsigned char*)ptr_src);
        ptr_dest = (char*)ptr_dest + 1;
        ptr_src = (const char*)ptr_src + 1;
    }
    return original_ptr;
}

Ghi chú kỹ thuật: Việc ép kiểu (unsigned char*) được thực hiện trước phép giải tham chiếu * là bắt buộc để tăng/trừ con trỏ theo đơn vị byte thay vì đơn vị kiểu mặc định của trỏ void.


Hàm dịch chuyển bộ nhớ an toàn memmove

Sự khác biệt cốt lõi

Khác với memcpy, hàm memmove được thiết kế đặc biệt để xử lý tình huống vùng nguồn và vùng đích bị trùng nhau (overlapping). Nếu xảy ra chồng lấn, hành vi của memcpy là không xác định, trong khi memmove sẽ luôn thực hiện đúng.

void * memmove(void * destination, const void * source, size_t num);

Xử lý chồng lấn bộ nhớ

Khi dịch chuyển dữ liệu trong cùng một mảng, ví dụ như dịch trái hoặc phải một đoạn văn bản, ta phải dùng hàm này để tránh dữ liệu gốc bị ghi đè lên trước khi được sao chép xong.

#include <stdio.h>
#include <string.h>
#include <assert.h>

int main() {
    // Tạo mảng chung làm nơi lưu trữ
    int buffer[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
    // Dịch chuyển 5 phần tử đầu tiên vào vị trí index 2
    // Vùng [2] đến [6] sẽ ghi đè lên vùng đang đọc nếu không cẩn thận
    memmove(buffer + 2, buffer, 5 * sizeof(int));

    for (int i = 0; i < 10; i++) {
        printf("%d ", buffer[i]);
    }
    return 0;
}

Triển khai thuật toán thông minh

Để xử lý chồng lấn, thuật toán cần kiểm tra vị trí tương đối của hai con trỏ:

  • Nếu đích nhỏ hơn nguồn (dest < src): Dữ liệu sẽ được ghi vào vùng trống, có thể sao chép từ trước ra sau.
  • Nếu đích lớn hơn nguồn (dest > src): Dữ liệu đích nằm phía sau nguồn, nếu copy từ trước ra sau thì nguồn sẽ bị ghi đè. Khi này phải copy từ sau ra trước.
void* safe_memmove(void* dst, const void* src, size_t len) {
    assert(dst && src);
    void* ret_val = dst;
    unsigned char* udst = (unsigned char*)dst;
    const unsigned char* usrc = (const unsigned char*)src;

    if (udst >= usrc && udst < usrc + len) {
        // Trường hợp chồng lấn: Copy ngược từ cuối lên đầu
        while (len--) {
            udst[len] = usrc[len];
        }
    } else {
        // Không chồng lấn: Copy xuôi từ đầu xuống cuối
        while (len--) {
            *udst++ = *usrc++;
        }
    }
    return ret_val;
}

Hàm khởi tạo giá trị bộ nhớ memset

Mục đích sử dụng

Hàm memset dùng để đặt tất cả các byte trong một vùng nhớ thành một giá trị byte cụ thể. Nó không đặt giá trị theo kiểu dữ liệu (như int hay float) mà theo từng byte độc lập.

void * memset(void * ptr, int val, size_t n);

Lỗi phổ biến khi sử dụng

Nhiều lập trình viên nhầm lẫn khi cố gắng gán giá trị 1 cho mảng số nguyên bằng hàm này.

#include <string.h>
#include <stdio.h>

int main() {
    int numbers[5] = { 0 };
    
    // Sai lầm: memset ghi giá trị 0x01 cho MỖI BYTE
    // Với số nguyên 4 byte, kết quả nhị phân là 0x01010101 (16843009) chứ không phải 1
    memset(numbers, 1, sizeof(numbers)); 

    for(int i=0; i<5; i++) {
        printf("%d ", numbers[i]); 
    }
    return 0;
}

Để khởi tạo mảng số nguyên thành 0, memset vẫn hoạt động vì bit của số 0 cũng là 0. Tuy nhiên, với các giá trị khác 0, nên sử dụng vòng lặp hoặc memset với giá trị phù hợp cho mục đích xóa bộ nhớ (zero-initialization).


Hàm so sánh bộ nhớ memcmp

Logic so sánh

Hàm này so sánh nội dung byte giữa hai vùng nhớ và trả về một số nguyên phản ánh mối quan hệ giữa chúng dựa trên giá trị vô hướng (unsigned char) của các byte không khớp đầu tiên.

int memcmp(const void * str1, const void * str2, size_t n);
  • Trả về < 0: Byte khác biệt đầu tiên trong str1 nhỏ hơn str2.
  • Trả về 0: Hai vùng nội dung hoàn toàn giống nhau.
  • Trả về > 0: Byte khác biệt đầu tiên trong str1 lớn hơn str2.

Ứng dụng kiểm tra dữ liệu

Ví dụ dưới đây minh họa cách kiểm tra sự đồng nhất của hai mảng ký tự:

#include <stdio.h>
#include <string.h>

int main() {
    char data_a[] = "SystemCheck123";
    char data_b[] = "SystemCheck456";
    
    int result = memcmp(data_a, data_b, sizeof(data_a));
    
    if (result == 0) {
        printf("Dữ liệu giống hệt nhau.\n");
    } else if (result < 0) {
        printf("Buffer thứ nhất nhỏ hơn buffer thứ hai.\n");
    } else {
        printf("Buffer thứ nhất lớn hơn buffer thứ hai.\n");
    }
    return 0;
}

Thẻ: C memory-management memset memcpy system-programming

Đăng vào ngày 2 tháng 10 lúc 20:18