... chỉ đơn giản là giảm một con trỏ bên ngoài phạm vi được phân bổ có vẻ rất sơ sài đối với tôi. Đây có phải là hành vi "được phép" trong C không?
Được phép? Đúng. Ý tưởng tốt? Không thường xuyên.
C là một tốc ký cho ngôn ngữ lắp ráp và trong ngôn ngữ lắp ráp không có con trỏ, chỉ có địa chỉ bộ nhớ. Con trỏ của C là các địa chỉ bộ nhớ có hành vi phụ là tăng hoặc giảm theo kích thước của những gì chúng trỏ đến khi chịu số học. Điều này làm cho những điều sau đây tốt từ góc độ cú pháp:
double *p = (double *)0xdeadbeef;
--p; // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];
Mảng không thực sự là một điều trong C; chúng chỉ là các con trỏ tới các phạm vi bộ nhớ liền kề hoạt động giống như các mảng. Các []
nhà điều hành là một cách viết tắt để thực hiện con trỏ số học và dereferencing, vì vậy a[x]
trên thực tế phương tiện *(a + x)
.
Có những lý do hợp lệ để thực hiện những điều trên, chẳng hạn như một số thiết bị I / O có một vài double
s được ánh xạ vào 0xdeadbee7
và 0xdeadbeef
. Rất ít chương trình sẽ cần phải làm điều đó.
Khi bạn tạo địa chỉ của một cái gì đó, chẳng hạn như bằng cách sử dụng &
toán tử hoặc gọi malloc()
, bạn muốn giữ nguyên con trỏ ban đầu để bạn biết rằng những gì nó trỏ đến thực sự là một cái gì đó hợp lệ. Giảm con trỏ có nghĩa là một số mã sai lầm có thể cố gắng hủy đăng ký nó, nhận kết quả sai, ghi đè một cái gì đó hoặc, tùy thuộc vào môi trường của bạn, vi phạm phân đoạn. Điều này đặc biệt đúng với malloc()
, bởi vì bạn đã đặt gánh nặng lên bất cứ ai đang gọi free()
để nhớ vượt qua giá trị ban đầu và không phải một số phiên bản bị thay đổi sẽ khiến tất cả bị phá vỡ.
Nếu bạn cần mảng dựa trên 1 trong C, bạn có thể thực hiện một cách an toàn với chi phí phân bổ một yếu tố bổ sung sẽ không bao giờ được sử dụng:
double *array_create(size_t size) {
// Wasting one element, so don't allow it to be full-sized
assert(size < SIZE_MAX);
return malloc((size+1) * sizeof(double));
}
inline double array_index(double *array, size_t index) {
assert(array != NULL);
assert(index >= 1); // This is a 1-based array
return array[index];
}
Lưu ý rằng điều này không làm gì để bảo vệ chống lại vượt quá giới hạn trên, nhưng điều đó đủ dễ để xử lý.
Phụ lục:
Một số chương và câu từ bản nháp C99 (xin lỗi, đó là tất cả những gì tôi có thể liên kết đến):
§6.5.2.1.1 nói rằng biểu thức thứ hai ("khác") được sử dụng với toán tử đăng ký là loại số nguyên. -1
là một số nguyên và điều đó làm cho p[-1]
hợp lệ và do đó cũng làm cho con trỏ &(p[-1])
hợp lệ. Điều này không ngụ ý rằng việc truy cập bộ nhớ tại vị trí đó sẽ tạo ra hành vi được xác định, nhưng con trỏ vẫn là một con trỏ hợp lệ.
§6.5.2.2 nói rằng toán tử mảng con ước tính tương đương với việc thêm số phần tử vào con trỏ, do đó p[-1]
tương đương với *(p + (-1))
. Vẫn còn hiệu lực, nhưng có thể không tạo ra hành vi mong muốn.
§6.5.6.8 nói (nhấn mạnh của tôi):
Khi một biểu thức có kiểu số nguyên được thêm vào hoặc trừ đi từ một con trỏ, kết quả có loại toán hạng con trỏ.
... nếu biểu thức P
trỏ đến i
phần tử -th của một đối tượng mảng, các biểu thức (P)+N
(tương đương, N+(P)
) và (P)-N
(trong đó N
có giá trị n
) trỏ đến, tương ứng, các phần tử -th i+n
và
i−n
-th của đối tượng mảng, miễn là chúng tồn tại .
Điều này có nghĩa là kết quả của số học con trỏ phải trỏ đến một phần tử trong một mảng. Nó không nói rằng số học phải được thực hiện cùng một lúc. Vì thế:
double a[20];
// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];
double *p = a - 1; // This is just a pointer. No dereferencing.
double e = p[0]; // Does not point at any element of a; behavior is undefined.
double f = p[1]; // Points at element 0 of a; behavior is defined.
Tôi có khuyên bạn nên làm mọi thứ theo cách này? Tôi không, và câu trả lời của tôi giải thích tại sao.