Triển khai mảng động trong C: Không cần Struct, không lưu trữ dung lượng
Một kỹ thuật lập trình thú vị trong ngôn ngữ C cho phép tạo mảng động tổng quát mà không cần định nghĩa struct hay lưu trữ biến dung lượng. Phương pháp này sử dụng mảng hai con trỏ để quản lý độ dài và dữ liệu, tận dụng các tính năng của C23 và GNU C.

Triển khai mảng động trong C: Không cần Struct, không lưu trữ dung lượng
Mảng động là một cấu trúc dữ liệu phổ biến, nhưng trong ngôn ngữ C, việc triển khai chúng thường đòi hỏi phải định nghĩa các struct phức tạp để quản lý độ dài và dung lượng. Tuy nhiên, một đoạn mã mới đây đã giới thiệu một cách tiếp cận tối giản và đầy sáng tạo để giải quyết vấn đề này.
Cách hoạt động
Thay vì sử dụng struct, kỹ thuật này sử dụng một mảng gồm hai con trỏ để quản lý dữ liệu:
- Con trỏ đầu tiên (
vec[0]) lưu trữ độ dài của mảng (dưới dạnguintptr_t). - Con trỏ thứ hai (
vec[1]) trỏ đến vùng dữ liệu thực tế.
Ví dụ, int *vec[2] = { 0 }; sẽ tạo ra một mảng động rỗng chứa các số nguyên, trong khi struct person *people[2] = { 0 }; tạo ra mảng động rỗng cho các cấu trúc person. Macro vec_push được cung cấp để thêm giá trị vào cuối mảng và trả về true nếu thành công. Đoạn mã này dựa trên tiêu chuẩn C23 kết hợp với biểu thức câu lệnh (statement expressions) - một tính năng của GNU C.
Tại sao kỹ thuật này thú vị?
Điểm thú vị đầu tiên là sự loại bỏ hoàn toàn struct. Bạn không cần phải đặt tên cho các kiểu cấu trúc dữ liệu riêng biệt (ví dụ: không cần IntVec hay PersonVec), giúp mã nguồn trở nên gọn gàng và linh hoạt hơn khi xử lý các kiểu dữ liệu khác nhau.
Thứ hai, và quan trọng hơn, là kỹ thuật này không lưu trữ dung lượng (capacity). Thay vào đó, dung lượng được tính toán khi cần thiết. Cụ thể, khi độ dài của mảng bằng 0 hoặc là lũy thừa của 2, hàm realloc sẽ được gọi để cấp phát dung lượng cho lũy thừa của 2 tiếp theo lớn hơn độ dài hiện tại.
Những hạn chế cần lưu ý
Mặc dù rất sáng tạo, phương pháp này có một số nhược điểm nhất định. Nó dựa vào hành vi được xác định bởi việc triển khai (implementation-defined behavior), cụ thể là việc ép kiểu giữa con trỏ và uintptr_t phải đảm bảo tính toàn vẹn của dữ liệu khi đọc lại.
Ngoài ra, do dung lượng không được lưu trữ tĩnh, việc "đặt trước" (reserve) một lượng bộ nhớ lớn trở nên khó khăn hơn. Khi đẩy dữ liệu vào, nếu độ dài đạt đến lũy thừa của 2, realloc sẽ tự động được gọi để mở rộng, điều này có thể làm vô hiệu hóa việc đặt trước bộ nhớ thủ công của người lập trình.



