Làm thế nào để đoạn mã này xác định kích thước mảng mà không sử dụng sizeof ()?


134

Đi qua một số câu hỏi phỏng vấn C, tôi đã tìm thấy một câu hỏi cho biết "Làm thế nào để tìm kích thước của một mảng trong C mà không cần sử dụng toán tử sizeof?", Với giải pháp sau. Nó hoạt động, nhưng tôi không thể hiểu tại sao.

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

Như mong đợi, nó trả về 5.

chỉnh sửa: mọi người đã chỉ ra câu trả lời này , nhưng cú pháp không khác một chút, tức là phương pháp lập chỉ mục

size = (&arr)[1] - arr;

vì vậy tôi tin rằng cả hai câu hỏi đều hợp lệ và có cách tiếp cận vấn đề hơi khác nhau. Cảm ơn tất cả các bạn đã giúp đỡ rất nhiều và giải thích kỹ lưỡng!


13
Chà, không thể tìm thấy nó, nhưng có vẻ như nói đúng ra. Phụ lục J.2 được nêu rõ ràng: Toán hạng của toán tử unary * có giá trị không hợp lệ là một hành vi không xác định. Ở đây &a + 1không chỉ đến bất kỳ đối tượng hợp lệ, vì vậy nó không hợp lệ.
Eugene Sh.



@AlmaDo cũng cú pháp khác nhau một chút, tức là phần lập chỉ mục, vì vậy tôi tin rằng câu hỏi này vẫn còn hiệu lực, nhưng tôi có thể sai. Cảm ơn vì chỉ ra điều ấy!
janojlic

1
@janojlicz Về cơ bản chúng giống nhau, vì (ptr)[x]giống nhau *((ptr) + x).
SS Anne

Câu trả lời:


135

Khi bạn thêm 1 vào một con trỏ, kết quả là vị trí của đối tượng tiếp theo trong một chuỗi các đối tượng của kiểu trỏ tới (nghĩa là một mảng). Nếu ptrỏ đến một intđối tượng, sau đó p + 1sẽ trỏ đến tiếp theo inttrong một chuỗi. Nếu ptrỏ đến một mảng 5 phần tử của int(trong trường hợp này là biểu thức &a), thì nó p + 1sẽ trỏ đến mảng 5 phần tửint tiếp theo của một chuỗi.

Trừ hai con trỏ (với điều kiện cả hai đều trỏ vào cùng một đối tượng mảng hoặc một con trỏ trỏ qua phần tử cuối cùng của mảng) sẽ tạo ra số lượng đối tượng (phần tử mảng) giữa hai con trỏ đó.

Biểu thức &amang lại địa chỉ của avà có kiểu int (*)[5](con trỏ tới mảng 5 phần tử của int). Biểu thức &a + 1mang lại địa chỉ của mảng 5 phần tử inttiếp theo avà cũng có kiểu int (*)[5]. Biểu thức *(&a + 1)hủy bỏ kết quả của &a + 1, sao cho nó mang lại địa chỉ của phần tử đầu tiên inttheo sau phần tử cuối cùng avà có loại int [5], trong ngữ cảnh này "phân rã" thành một biểu thức loại int *.

Tương tự, biểu thức a"phân rã" thành một con trỏ tới phần tử đầu tiên của mảng và có kiểu int *.

Một hình ảnh có thể giúp:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

Đây là hai chế độ xem của cùng một bộ lưu trữ - ở bên trái, chúng tôi đang xem nó dưới dạng một chuỗi gồm 5 phần tử int, trong khi ở bên phải, chúng tôi đang xem nó như một chuỗi int. Tôi cũng hiển thị các biểu thức khác nhau và các loại của họ.

Hãy lưu ý, biểu thức *(&a + 1)dẫn đến hành vi không xác định :

...
Nếu kết quả chỉ ra một phần tử qua phần tử cuối cùng của đối tượng mảng, thì nó sẽ không được sử dụng làm toán hạng của toán tử unary * được ước tính.

Dự thảo trực tuyến C 2011 , 6.5.6 / 9


13
Rằng không được sử dụng văn bản trên mạng là chính thức: C 2018 6.5.6 8.
Eric Postpischil

@EricPostpischil: Bạn có liên kết đến dự thảo trước quán rượu năm 2018 (tương tự N1570.pdf) không?
John Bode

1
@JohnBode: Câu trả lời này có liên kết đến Wayback Machine . Tôi đã kiểm tra tiêu chuẩn chính thức trong bản sao đã mua của tôi.
Eric Postpischil

7
Vì vậy, nếu một người viết size = (int*)(&a + 1) - a;mã này sẽ hoàn toàn hợp lệ? : o
Gizmo

@Gizmo ban đầu họ có thể không viết điều đó bởi vì theo cách đó bạn phải chỉ định loại phần tử; bản gốc có thể được viết được định nghĩa là một macro để sử dụng chung loại trên các loại phần tử khác nhau.
Leushenko

35

Dòng này là quan trọng nhất:

size = *(&a + 1) - a;

Như bạn có thể thấy, đầu tiên nó lấy địa chỉ avà thêm một địa chỉ vào đó. Sau đó, nó hủy bỏ con trỏ và trừ giá trị ban đầu của anó.

Số học con trỏ trong C làm cho điều này trả về số lượng phần tử trong mảng, hoặc 5. Thêm một và &alà một con trỏ đến mảng tiếp theo sau 5 intgiây a. Sau đó, mã này hủy bỏ con trỏ kết quả và trừ a(một kiểu mảng đã phân rã thành một con trỏ) từ đó, đưa ra số lượng phần tử trong mảng.

Chi tiết về cách hoạt động của số học con trỏ:

Giả sử bạn có một con trỏ trỏ xyzđến một intloại và chứa giá trị (int *)160. Khi bạn trừ đi bất kỳ số nào từ xyz, C chỉ định rằng số tiền thực được trừ xyzlà số đó nhân với kích thước của loại mà nó trỏ đến. Ví dụ, nếu bạn trừ 5từ xyz, giá trị của xyzkết quả sẽ được xyz - (sizeof(*xyz) * 5)nếu con trỏ số học không được áp dụng.

amột mảng các 5 intloại, giá trị kết quả sẽ là 5. Tuy nhiên, điều này sẽ không hoạt động với một con trỏ, chỉ với một mảng. Nếu bạn thử điều này với một con trỏ, kết quả sẽ luôn như vậy 1.

Đây là một ví dụ nhỏ cho thấy các địa chỉ và làm thế nào điều này không được xác định. Phía bên trái hiển thị các địa chỉ:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

Đây có nghĩa là mã được trừ atừ &a[5](hoặc a+5), đưa ra 5.

Lưu ý rằng đây là hành vi không xác định và không nên được sử dụng trong mọi trường hợp. Đừng hy vọng hành vi này sẽ nhất quán trên tất cả các nền tảng và không sử dụng nó trong các chương trình sản xuất.


27

Hmm, tôi nghi ngờ đây là thứ sẽ không hoạt động trở lại trong những ngày đầu của C. Mặc dù vậy, nó rất thông minh.

Thực hiện từng bước một:

  • &a lấy một con trỏ tới một đối tượng kiểu int [5]
  • +1 được đối tượng tiếp theo như vậy giả sử có một mảng của những
  • * chuyển đổi hiệu quả địa chỉ đó thành kiểu con trỏ thành int
  • -a trừ hai con trỏ int, trả về số lượng int giữa chúng.

Tôi không chắc nó hoàn toàn hợp pháp (trong trường hợp này, ý tôi là luật sư ngôn ngữ - sẽ không hoạt động trong thực tế), do một số hoạt động loại đang diễn ra. Ví dụ, bạn chỉ "được phép" trừ hai con trỏ khi chúng trỏ đến các phần tử trong cùng một mảng. *(&a+1)được tổng hợp bằng cách truy cập vào một mảng khác, mặc dù là một mảng cha, vì vậy thực tế không phải là một con trỏ vào cùng một mảng như a. Ngoài ra, trong khi bạn được phép tổng hợp một con trỏ qua phần tử cuối cùng của một mảng và bạn có thể coi bất kỳ đối tượng nào là một mảng của 1 phần tử, hoạt động của dereferences ( *) không được "cho phép" trên con trỏ được tổng hợp này, mặc dù nó không có hành vi trong trường hợp này!

Tôi nghi ngờ rằng trong những ngày đầu của C (cú pháp K & R, có ai không?), Một mảng phân rã thành một con trỏ nhanh hơn nhiều, vì vậy *(&a+1)chỉ có thể trả về địa chỉ của con trỏ tiếp theo của kiểu int **. Các định nghĩa khắt khe hơn về C ++ hiện đại chắc chắn cho phép con trỏ tồn tại kiểu mảng và biết kích thước mảng, và có lẽ các tiêu chuẩn C đã tuân theo. Tất cả mã chức năng C chỉ lấy con trỏ làm đối số, vì vậy sự khác biệt rõ ràng về mặt kỹ thuật là tối thiểu. Nhưng tôi chỉ đoán ở đây.

Loại câu hỏi về tính hợp pháp chi tiết này thường áp dụng cho trình thông dịch C hoặc công cụ loại lint, thay vì mã được biên dịch. Trình thông dịch có thể triển khai một mảng 2D dưới dạng một mảng các con trỏ tới các mảng, bởi vì có một tính năng thời gian chạy ít hơn để thực hiện, trong trường hợp đó, việc hủy bỏ +1 sẽ gây tử vong và ngay cả khi nó hoạt động sẽ trả lời sai.

Một điểm yếu khác có thể là trình biên dịch C có thể căn chỉnh mảng bên ngoài. Hãy tưởng tượng nếu đây là một mảng gồm 5 ký tự ( char arr[5]), khi chương trình thực hiện &a+1thì nó đang gọi hành vi "mảng của mảng". Trình biên dịch có thể quyết định rằng một mảng gồm 5 ký tự ( char arr[][5]) thực sự được tạo ra như một mảng gồm 8 ký tự ( char arr[][8]), để mảng bên ngoài sắp xếp độc đáo. Mã mà chúng ta đang thảo luận bây giờ sẽ báo cáo kích thước mảng là 8, không phải 5. Tôi không nói rằng một trình biên dịch cụ thể chắc chắn sẽ làm điều này, nhưng nó có thể.


Đủ công bằng. Tuy nhiên vì lý do khó giải thích, mọi người đều sử dụng sizeof () / sizeof ()?
Gem Taylor

5
Hầu hết mọi người làm. Ví dụ, sizeof(array)/sizeof(array[0])đưa ra số lượng phần tử trong một mảng.
SS Anne

Trình biên dịch C được phép căn chỉnh mảng, nhưng tôi không tin nó được phép thay đổi loại mảng sau khi thực hiện. Sắp xếp sẽ được thực hiện thực tế hơn bằng cách chèn byte đệm.
Kevin

1
Việc trừ các con trỏ không chỉ giới hạn ở hai con trỏ vào cùng một mảng. Các con trỏ cũng được phép đi qua một phần cuối của mảng. &a+1được định nghĩa. Như John Bollinger lưu ý, *(&a+1)là không, vì nó cố gắng để hủy bỏ một đối tượng không tồn tại.
Eric Postpischil

5
Một trình biên dịch không thể thực hiện char [][5]như là char arr[][8]. Một mảng chỉ là các đối tượng lặp đi lặp lại trong đó; không có đệm. Ngoài ra, điều này sẽ phá vỡ ví dụ 2 (không quy tắc) trong C 2018 6.5.3.4 7, cho chúng ta biết chúng ta có thể tính số lượng phần tử trong một mảng với sizeof array / sizeof array[0].
Eric Postpischil
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.