Một đối số free(void *)
(được giới thiệu trong Unix V7) có một lợi thế lớn khác so với hai đối số mfree(void *, size_t)
trước đó mà tôi chưa thấy đề cập ở đây: một đối số free
đơn giản hóa đáng kể mọi API khác hoạt động với bộ nhớ heap. Ví dụ, nếu free
cần kích thước của khối bộ nhớ, thì strdup
bằng cách nào đó sẽ phải trả về hai giá trị (con trỏ + kích thước) thay vì một (con trỏ) và C làm cho trả về nhiều giá trị cồng kềnh hơn nhiều so với trả về một giá trị. Thay vì char *strdup(char *)
chúng ta phải viết char *strdup(char *, size_t *)
hoặc viết khác struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
. (Ngày nay, tùy chọn thứ hai đó trông khá hấp dẫn, bởi vì chúng tôi biết rằng chuỗi kết thúc bằng NUL là "lỗi thiết kế thảm khốc nhất trong lịch sử máy tính", nhưng đó là cách nói nhận thức. Trở lại những năm 70, khả năng xử lý các chuỗi đơn giản của C char *
thực sự được coi là một lợi thế xác định so với các đối thủ cạnh tranh như Pascal và Algol .) Thêm vào đó, nó không chỉ strdup
gặp phải vấn đề này - nó ảnh hưởng đến mọi hệ thống hoặc do người dùng định nghĩa hàm phân bổ bộ nhớ heap.
Các nhà thiết kế Unix ban đầu là những người rất thông minh, và có nhiều lý do tại sao lại free
tốt hơn mfree
về cơ bản, tôi nghĩ câu trả lời cho câu hỏi là họ nhận thấy điều này và thiết kế hệ thống của họ cho phù hợp. Tôi nghi ngờ bạn sẽ tìm thấy bất kỳ bản ghi trực tiếp nào về những gì đang diễn ra trong đầu họ vào thời điểm họ đưa ra quyết định đó. Nhưng chúng ta có thể tưởng tượng.
Giả sử bạn đang viết ứng dụng trong C để chạy trên V6 Unix, với hai đối số của nó mfree
. Cho đến nay, bạn đã quản lý tốt, nhưng việc theo dõi các kích thước con trỏ này ngày càng trở nên phức tạp hơn khi các chương trình của bạn trở nên tham vọng hơn và ngày càng yêu cầu sử dụng nhiều hơn các biến được phân bổ theo đống. Nhưng sau đó bạn có một ý tưởng tuyệt vời: thay vì sao chép những thứ này size_t
mọi lúc, bạn có thể chỉ cần viết một số hàm tiện ích, lưu trữ kích thước trực tiếp bên trong bộ nhớ được cấp phát:
void *my_alloc(size_t size) {
void *block = malloc(sizeof(size) + size);
*(size_t *)block = size;
return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
block = (size_t *)block - 1;
mfree(block, *(size_t *)block);
}
Và bạn càng viết nhiều mã bằng cách sử dụng các chức năng mới này, chúng càng có vẻ tuyệt vời hơn. Chúng không chỉ làm cho mã của bạn dễ viết hơn mà còn làm cho mã của bạn nhanh hơn - hai điều không thường đi cùng nhau! Trước khi bạn chuyển các ký tự này đi size_t
khắp nơi, điều này làm tăng chi phí CPU cho quá trình sao chép và có nghĩa là bạn phải đổ các thanh ghi thường xuyên hơn (đặc biệt là đối với các đối số hàm bổ sung) và lãng phí bộ nhớ (vì các lệnh gọi hàm lồng nhau thường sẽ dẫn đến trong nhiều bản sao size_t
đang được lưu trữ trong các khung ngăn xếp khác nhau). Trong hệ thống mới của bạn, bạn vẫn phải dành bộ nhớ để lưu trữsize_t
, nhưng chỉ một lần và nó không bao giờ bị sao chép ở bất kỳ đâu. Đây có vẻ như là những hiệu quả nhỏ, nhưng hãy nhớ rằng chúng ta đang nói về các máy cao cấp với 256 KiB RAM.
Điều này làm cho bạn hạnh phúc! Vì vậy, bạn chia sẻ mẹo hay của mình với những đấng mày râu đang làm việc trên bản phát hành Unix tiếp theo, nhưng nó không làm họ hài lòng mà còn khiến họ buồn. Bạn thấy đấy, họ chỉ đang trong quá trình thêm một loạt các chức năng tiện ích mới như strdup
, và họ nhận ra rằng những người sử dụng mẹo hay của bạn sẽ không thể sử dụng các chức năng mới của họ, bởi vì các chức năng mới của họ đều sử dụng con trỏ + kích thước cồng kềnh API. Và điều đó cũng khiến bạn buồn, vì bạn nhận ra rằng mình sẽ phải tự viết lại strdup(char *)
hàm tốt trong mỗi chương trình bạn viết, thay vì có thể sử dụng phiên bản hệ thống.
Nhưng đợi đã! Đây là năm 1977 và khả năng tương thích ngược sẽ không được phát minh trong 5 năm nữa! Và bên cạnh đó, không ai nghiêm túc thực sự sử dụng thứ "Unix" khó hiểu này với cái tên không màu sắc của nó. Ấn bản đầu tiên của K&R hiện đang trên đường đến nhà xuất bản, nhưng điều đó không thành vấn đề - nó nói ngay trên trang đầu tiên rằng "C không cung cấp hoạt động nào để xử lý trực tiếp các đối tượng tổng hợp như chuỗi ký tự ... không có đống ... ”. Tại thời điểm này trong lịch sử, string.h
và malloc
là phần mở rộng của nhà cung cấp (!). Vì vậy, Be râu Man # 1 đề xuất, chúng ta có thể thay đổi chúng theo cách nào chúng ta muốn; tại sao chúng tôi không tuyên bố người phân bổ khó khăn của bạn là người phân bổ chính thức ?
Vài ngày sau, Be râu Man # 2 nhìn thấy API mới và nói này, chờ đã, điều này tốt hơn trước, nhưng nó vẫn dành toàn bộ từ mỗi phân bổ để lưu trữ kích thước. Anh ta xem đây là điều tiếp theo để báng bổ. Mọi người khác nhìn anh ấy như thể anh ấy bị điên, vì bạn còn có thể làm gì nữa? Đêm đó, anh ấy ở lại muộn và phát minh ra một trình phân bổ mới không lưu trữ kích thước nào cả, nhưng thay vào đó, suy diễn nó nhanh chóng bằng cách thực hiện các phép bit ma thuật đen trên giá trị con trỏ và hoán đổi nó trong khi giữ nguyên API mới. API mới có nghĩa là không ai nhận thấy sự chuyển đổi, nhưng họ nhận thấy rằng sáng hôm sau trình biên dịch sử dụng RAM ít hơn 10%.
Và bây giờ mọi người đều hạnh phúc: Bạn nhận được mã dễ viết hơn và nhanh hơn, Người đàn ông có râu số 1 viết một cách đơn giản strdup
mà mọi người sẽ thực sự sử dụng và Người đàn ông có râu số 2 - tự tin rằng anh ấy đã kiếm được tiền của mình - - quay lại với việc lộn xộn với quines . Gửi nó!
Hoặc ít nhất, đó là cách nó có thể xảy ra.