Lập chỉ mục con trỏ


11

Tôi hiện đang đọc một cuốn sách có tiêu đề "Công thức toán số trong C". Trong cuốn sách này, tác giả nêu chi tiết cách các thuật toán nhất định vốn hoạt động tốt hơn nếu chúng ta có các chỉ số bắt đầu bằng 1 (Tôi không hoàn toàn tuân theo lập luận của mình và đó không phải là điểm của bài đăng này), nhưng C luôn lập chỉ mục các mảng bắt đầu bằng 0 Để giải quyết vấn đề này, ông đề nghị chỉ cần giảm con trỏ sau khi cấp phát, ví dụ:

float *a = malloc(size);
a--;

Điều này, theo ông, sẽ thực sự cung cấp cho bạn một con trỏ có chỉ số bắt đầu bằng 1, sau đó sẽ được miễn phí với:

free(a + 1);

Tuy nhiên, theo như tôi biết, đây là hành vi không xác định theo tiêu chuẩn C. Đây rõ ràng là một cuốn sách rất có uy tín trong cộng đồng HPC, vì vậy tôi không muốn đơn giản coi thường những gì anh ta nói, mà 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? Tôi đã thử nghiệm nó bằng cả gcc và icc, và cả hai kết quả đó dường như cho thấy tôi không lo lắng gì cả, nhưng tôi muốn hoàn toàn tích cực.


3
những gì C tiêu chuẩn nào bạn tham khảo? Tôi hỏi bởi vì theo hồi ức của tôi, "Công thức số trong C" đã được xuất bản vào những năm 1990, vào thời cổ đại của K & R và có thể là ANSI C
gnat

2
Câu hỏi SO liên quan: stackoverflow.com/questions/10473573/ Từ
dan04

3
"Tôi đã thử nghiệm nó bằng cả gcc và icc, và cả hai kết quả đó dường như cho thấy tôi không lo lắng gì cả nhưng tôi muốn hoàn toàn tích cực." Đừng bao giờ cho rằng vì trình biên dịch của bạn cho phép nó, ngôn ngữ C cho phép nó. Tất nhiên trừ khi bạn ổn với việc phá mã trong tương lai.
Doval

5
Không muốn trở nên lén lút, "Công thức số" thường được coi là một cuốn sách hữu ích, nhanh chóng và bẩn thỉu, không phải là mô hình phát triển phần mềm hoặc phân tích số. Kiểm tra bài viết trên Wikipedia về "Công thức số" để biết tóm tắt về một số lời chỉ trích.
Charles E. Grant

1
Bên cạnh, đây là lý do tại sao chúng tôi lập chỉ mục từ số không: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Russell Borogove

Câu trả lời:


16

Bạn đúng mã đó như

float a = malloc(size);
a--;

mang lại hành vi không xác định, theo tiêu chuẩn ANSI C, phần 3.3.6:

Trừ khi cả toán hạng con trỏ và điểm kết quả đến một thành viên của cùng một đối tượng mảng hoặc một lần qua thành viên cuối cùng của đối tượng mảng, hành vi không được xác định

Đối với mã như thế này, chất lượng của mã C trong cuốn sách (trở lại khi tôi sử dụng nó vào cuối những năm 1990) không được coi là rất cao.

Vấn đề với hành vi không xác định là cho dù trình biên dịch tạo ra kết quả nào, kết quả đó là theo định nghĩa chính xác (ngay cả khi nó có tính phá hủy cao và không thể đoán trước).
May mắn thay, rất ít trình biên dịch nỗ lực thực sự gây ra hành vi không mong muốn cho các trường hợp như vậy và việc malloctriển khai điển hình trên các máy được sử dụng cho HPC có một số dữ liệu sổ sách ngay trước địa chỉ mà nó trả về, do đó, phần giảm thường sẽ cung cấp cho bạn một con trỏ vào dữ liệu sổ sách đó. Viết ở đó không phải là một ý tưởng tốt, nhưng chỉ cần tạo con trỏ là vô hại trên các hệ thống đó.

Chỉ cần lưu ý rằng mã có thể bị hỏng khi môi trường thời gian chạy bị thay đổi hoặc khi mã được chuyển sang một môi trường khác.


4
Chính xác, có thể trên kiến ​​trúc đa ngân hàng, malloc có thể cung cấp cho bạn địa chỉ số 0 trong một ngân hàng và việc giảm nó có thể gây ra bẫy CPU với một dòng chảy cho một cái.
Vality

1
Tôi không đồng ý rằng đó là "may mắn". Tôi nghĩ sẽ tốt hơn nhiều nếu trình biên dịch phát ra mã bị lỗi ngay lập tức bất cứ khi nào bạn gọi hành vi không xác định.
David Conrad

4
@DavidConrad: Sau đó, C không phải là ngôn ngữ dành cho bạn. Phần lớn hành vi không xác định trong C không thể được phát hiện dễ dàng hoặc chỉ với một cú đánh hiệu suất nghiêm trọng.
Bart van Ingen Schenau

Tôi đã nghĩ đến việc thêm "với một trình chuyển đổi trình biên dịch". Rõ ràng bạn sẽ không muốn điều đó cho mã được tối ưu hóa. Nhưng, bạn đã đúng, và đó là lý do tại sao tôi đã từ bỏ việc viết C mười năm trước.
David Conrad

@BartvanIngenSchenau tùy thuộc vào ý nghĩa của bạn về 'hiệu suất nghiêm trọng', có sự thực thi tượng trưng cho C (ví dụ clang + klee) cũng như các chất khử trùng (asan, tsan, ubsan, valgrind, v.v.)
Maciej Piechotka

10

Chính thức, đó là hành vi không xác định để có một điểm con trỏ bên ngoài mảng (ngoại trừ một lần qua cuối), ngay cả khi nó không bao giờ được quy định .

Trong thực tế, nếu bộ xử lý của bạn có mô hình bộ nhớ phẳng (trái ngược với các bộ nhớ lạ như x86-16 ) và nếu trình biên dịch không cung cấp cho bạn lỗi thời gian chạy hoặc tối ưu hóa không chính xác nếu bạn tạo một con trỏ không hợp lệ, thì mã sẽ hoạt động. bình thường.


1
Điều đó có ý nghĩa. Thật không may, đó là hai quá nhiều nếu theo ý thích của tôi.
wolfPack88

3
Điểm cuối cùng là IMHO có vấn đề nhất. Vì trình biên dịch đôi khi không để xảy ra bất cứ điều gì nền tảng "tự nhiên" làm trong trường hợp của UB, nhưng trình tối ưu hóa đang tích cực khai thác nó, tôi sẽ không chơi với nó quá nhẹ.
Matteo Italia

3

Đầu tiên, đó là hành vi không xác định. Một số trình biên dịch tối ưu hóa ngày nay trở nên rất tích cực về hành vi không xác định. Ví dụ, vì a-- trong trường hợp này là hành vi không xác định, trình biên dịch có thể quyết định lưu một lệnh và chu trình xử lý và không giảm a. Đó là chính thức và hợp pháp.

Bỏ qua điều đó, bạn có thể trừ 1, hoặc 2 hoặc 1980. Ví dụ: nếu tôi có dữ liệu tài chính trong những năm 1980 đến 2013, tôi có thể trừ 1980. Bây giờ nếu chúng ta sử dụng float * a = malloc (size); chắc chắn có một số hằng số k lớn sao cho a - k là một con trỏ null. Trong trường hợp đó, chúng tôi thực sự mong đợi một cái gì đó đi sai.

Bây giờ có một cấu trúc lớn, kích thước một megabyte. Phân bổ một con trỏ p chỉ vào hai cấu trúc. p - 1 có thể là một con trỏ null. p - 1 có thể bao quanh (nếu một cấu trúc là một megabyte và khối malloc là 900 KB từ khi bắt đầu không gian địa chỉ). Vì vậy, nó có thể không có bất kỳ ác ý nào của trình biên dịch mà p - 1> p. Mọi thứ có thể trở nên thú vị.


1

... 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 doubles được ánh xạ vào 0xdeadbee70xdeadbeef. 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. -1là 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 Ptrỏ đến iphầ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 đó Ncó giá trị n) trỏ đến, tương ứng, các phần tử -th i+ni−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.


8
-1 Định nghĩa 'được phép' bao gồm mã mà tiêu chuẩn C tuyên bố vì việc tạo kết quả không xác định không phải là một định nghĩa hữu ích.
Pete Kirkham

Những người khác đã chỉ ra rằng đó là hành vi không xác định, vì vậy bạn không nên nói rằng đó là "được phép". Tuy nhiên, đề xuất phân bổ thêm một phần tử không sử dụng 0 là tốt.
200_success

Điều này thực sự không đúng, ít nhất xin lưu ý rằng điều này bị cấm theo tiêu chuẩn C.
Vality

@PeteKirkham: Tôi không đồng ý. Xem phần phụ lục cho câu trả lời của tôi.
Blrfl

4
@Blrfl 6.5.6 của trạng thái tiêu chuẩn ISO C11 trong trường hợp thêm một số nguyên vào một con trỏ: "Nếu cả toán hạng con trỏ và kết quả trỏ đến các phần tử của cùng một đối tượng mảng hoặc vượt qua phần tử cuối cùng của đối tượng mảng , việc đánh giá sẽ không tạo ra tràn, nếu không, hành vi không được xác định. "
Vality
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.