Tại sao trình biên dịch C và C ++ cho phép độ dài mảng trong chữ ký hàm khi chúng không bao giờ được thi hành?


131

Đây là những gì tôi tìm thấy trong thời gian học:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

Vì vậy, trong biến int dis(char a[1]), [1]dường như không làm gì cả và không hoạt động
, bởi vì tôi có thể sử dụng a[2]. Chỉ thích int a[]hoặc char *a. Tôi biết tên mảng là một con trỏ và cách truyền tải một mảng, vì vậy câu đố của tôi không phải là về phần này.

Những gì tôi muốn biết là tại sao trình biên dịch cho phép hành vi này ( int a[1]). Hay nó có ý nghĩa khác mà tôi không biết?


6
Đó là bởi vì bạn thực sự không thể truyền mảng cho các hàm.
Ed S.

37
Tôi nghĩ câu hỏi ở đây là tại sao C cho phép bạn khai báo một tham số thuộc kiểu mảng khi nó sẽ hoạt động chính xác như một con trỏ.
Brian

8
@Brian: Tôi không chắc đây là đối số cho hay chống lại hành vi, nhưng nó cũng áp dụng nếu loại đối số là loại typedefcó mảng. Vì vậy, "phân rã thành con trỏ" trong các loại đối số không chỉ là cú pháp thay thế []bằng *, nó thực sự đi qua hệ thống loại. Điều này có hậu quả trong thế giới thực đối với một số loại tiêu chuẩn như thế va_listcó thể được xác định bằng loại mảng hoặc loại không mảng.
R .. GitHub DỪNG GIÚP ICE

4
@songyuanyao Bạn có thể thực hiện một cái gì đó không hoàn toàn giống nhau trong C (và C ++) bằng cách sử dụng một con trỏ : int dis(char (*a)[1]). Sau đó, bạn chuyển một con trỏ tới một mảng : dis(&b). Nếu bạn sẵn sàng sử dụng các tính năng C không tồn tại trong C ++, bạn cũng có thể nói những điều như void foo(int data[static 256])int bar(double matrix[*][*]), nhưng đó là một loại sâu khác.
Stuart Olsen

1
@StuartOlsen Điểm không phải là tiêu chuẩn xác định cái gì. Vấn đề là tại sao bất cứ ai định nghĩa nó theo cách đó.
dùng253751

Câu trả lời:


156

Đây là một cách giải quyết cú pháp để truyền mảng cho các hàm.

Trên thực tế không thể truyền một mảng trong C. Nếu bạn viết cú pháp trông giống như nó sẽ vượt qua mảng, điều thực sự xảy ra là một con trỏ đến phần tử đầu tiên của mảng được truyền thay thế.

Vì con trỏ không bao gồm bất kỳ thông tin độ dài nào, nên nội dung của bạn []trong danh sách tham số chính thức của hàm thực sự bị bỏ qua.

Quyết định cho phép cú pháp này được đưa ra vào những năm 1970 và đã gây ra nhiều nhầm lẫn kể từ khi ...


21
Là một lập trình viên không phải C, tôi thấy câu trả lời này rất dễ tiếp cận. +1
asteri

21
+1 cho "Quyết định cho phép cú pháp này được đưa ra vào những năm 1970 và đã gây ra nhiều nhầm lẫn kể từ khi ..."
NoSenseEtAl

8
Điều này đúng nhưng cũng có thể vượt qua một mảng chỉ có kích thước đó bằng void foo(int (*somearray)[20])cú pháp. trong trường hợp này 20 được thi hành trên các trang web của người gọi.
v.oddou

14
-1 Là một lập trình viên C, tôi thấy câu trả lời này không chính xác. []không được bỏ qua trong các mảng nhiều chiều như trong câu trả lời của pat. Vì vậy, bao gồm cú pháp mảng là cần thiết. Ngoài ra, không có gì ngăn trình biên dịch đưa ra các cảnh báo ngay cả trên các mảng một chiều.
dùng694733

7
Bằng "nội dung của []" của bạn, tôi đang nói cụ thể về mã trong Câu hỏi. Cú pháp cú pháp này hoàn toàn không cần thiết, điều tương tự có thể đạt được bằng cách sử dụng cú pháp con trỏ, tức là nếu một con trỏ được thông qua thì yêu cầu tham số phải là một công cụ khai báo con trỏ. Ví dụ, trong ví dụ của pat, void foo(int (*args)[20]);Ngoài ra, nói đúng C không có mảng đa chiều; nhưng nó có các mảng có các phần tử có thể là các mảng khác. Điều này không thay đổi bất cứ điều gì.
MM

143

Độ dài của kích thước đầu tiên được bỏ qua, nhưng độ dài của các kích thước bổ sung là cần thiết để cho phép trình biên dịch tính toán bù đắp chính xác. Trong ví dụ sau, foohàm được truyền một con trỏ tới mảng hai chiều.

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

Kích thước của kích thước đầu tiên [10]được bỏ qua; trình biên dịch sẽ không ngăn bạn lập chỉ mục kết thúc (lưu ý rằng chính thức muốn 10 phần tử, nhưng thực tế chỉ cung cấp 2). Tuy nhiên, kích thước của kích thước thứ hai [20]được sử dụng để xác định bước tiến của mỗi hàng và ở đây, chính thức phải phù hợp với thực tế. Một lần nữa, trình biên dịch sẽ không ngăn bạn lập chỉ mục ở cuối chiều thứ hai.

Độ lệch byte từ gốc của mảng đến một phần tử args[row][col]được xác định bởi:

sizeof(int)*(col + 20*row)

Lưu ý rằng nếu col >= 20, thì bạn thực sự sẽ lập chỉ mục vào một hàng tiếp theo (hoặc tắt cuối toàn bộ mảng).

sizeof(args[0]), trả 80về máy của tôi đâu sizeof(int) == 4. Tuy nhiên, nếu tôi cố gắng thực hiện sizeof(args), tôi nhận được cảnh báo trình biên dịch sau:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

Ở đây, trình biên dịch cảnh báo rằng nó sẽ chỉ đưa ra kích thước của con trỏ mà mảng đã phân rã thay vì kích thước của chính mảng đó.


Rất hữu ích - tính nhất quán với điều này cũng hợp lý là lý do cho việc giải quyết vấn đề trong trường hợp 1-d.
jwg

1
Đó là ý tưởng tương tự như trường hợp 1-D. Những gì trông giống như một mảng 2-D trong C và C ++ thực sự là một mảng 1-D, mỗi phần tử là một mảng 1-D khác. Trong trường hợp này, chúng ta có một mảng có 10 phần tử, mỗi phần tử là "mảng 20 ints". Như được mô tả trong bài viết của tôi, những gì thực sự được truyền cho hàm là con trỏ tới phần tử đầu tiên của args. Trong trường hợp này, phần tử đầu tiên của args là "mảng 20 ints". Con trỏ bao gồm thông tin loại; những gì được thông qua là "con trỏ tới một mảng 20 ints".
MM

9
Yup, đó là những gì int (*)[20]loại; "con trỏ tới một mảng 20 ints".
vỗ

33

Vấn đề và cách khắc phục nó trong C ++

Vấn đề đã được giải thích rộng rãi bởi patMatt . Trình biên dịch về cơ bản bỏ qua kích thước đầu tiên của kích thước của mảng có hiệu quả bỏ qua kích thước của đối số được truyền.

Mặt khác, trong C ++, bạn có thể dễ dàng vượt qua giới hạn này theo hai cách:

  • sử dụng tài liệu tham khảo
  • sử dụng std::array(kể từ C ++ 11)

Người giới thiệu

Nếu chức năng của bạn chỉ cố đọc hoặc sửa đổi một mảng hiện có (không sao chép nó), bạn có thể dễ dàng sử dụng tài liệu tham khảo.

Ví dụ: giả sử bạn muốn có một hàm đặt lại một mảng mười ints cài đặt mọi phần tử thành 0. Bạn có thể dễ dàng làm điều đó bằng cách sử dụng chữ ký hàm sau:

void reset(int (&array)[10]) { ... }

Điều này không chỉ hoạt động tốt , mà còn thực thi kích thước của mảng .

Bạn cũng có thể sử dụng các mẫu để tạo mã chung ở trên :

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

Và cuối cùng bạn có thể tận dụng constsự đúng đắn. Hãy xem xét một chức năng in một mảng gồm 10 phần tử:

void show(const int (&array)[10]) { ... }

Bằng cách áp dụng constvòng loại, chúng tôi đang ngăn chặn các sửa đổi có thể .


Lớp thư viện chuẩn cho mảng

Nếu bạn xem xét cú pháp trên cả xấu và không cần thiết, như tôi làm, chúng ta có thể ném nó vào can và sử dụng std::arraythay thế (kể từ C ++ 11).

Đây là mã được cấu trúc lại:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

Thật tuyệt phải không? Chưa kể rằng thủ thuật mã chung mà tôi đã dạy trước đó, vẫn hoạt động:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Không chỉ vậy, nhưng bạn có được bản sao và di chuyển ngữ nghĩa miễn phí. :)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

Bạn đang chờ đợi điều gì? Đi sử dụng std::array.


2
@kietz, tôi xin lỗi, chỉnh sửa được đề xuất của bạn đã bị từ chối, nhưng chúng tôi tự động cho rằng C ++ 11 đang được sử dụng , trừ khi có quy định khác.
Giày

Điều này là đúng, nhưng chúng tôi cũng phải chỉ định xem có giải pháp nào chỉ là C ++ 11 hay không, dựa trên liên kết bạn đã đưa ra.
trlkly

@trlkly, tôi đồng ý. Tôi đã chỉnh sửa câu trả lời tương ứng. Cảm ơn đã chỉ ra điều đó.
Giày

9

Đây là một tính năng thú vị của C cho phép bạn tự bắn vào chân mình một cách hiệu quả nếu bạn quá nghiêng.

Tôi nghĩ lý do là C chỉ là một bước trên ngôn ngữ lắp ráp. Kiểm tra kích thướccác tính năng an toàn tương tự đã được gỡ bỏ để cho phép hiệu suất cao nhất, đó không phải là điều xấu nếu lập trình viên rất siêng năng.

Ngoài ra, việc gán kích thước cho đối số hàm có lợi thế là khi hàm được sử dụng bởi một lập trình viên khác, có khả năng họ sẽ nhận thấy hạn chế kích thước. Chỉ sử dụng một con trỏ không truyền đạt thông tin đó cho lập trình viên tiếp theo.


3
Đúng. C được thiết kế để tin tưởng người lập trình qua trình biên dịch. Nếu bạn đang lập chỉ mục một cách trắng trợn về sự kết thúc của một mảng, bạn phải làm điều gì đó đặc biệt và có chủ ý.
John

7
Tôi đã cắt răng trong lập trình trên C 14 năm trước. Trong tất cả các giáo sư của tôi đã nói, một cụm từ gắn bó với tôi hơn tất cả các cụm từ khác, "C được viết bởi các lập trình viên, dành cho lập trình viên." Ngôn ngữ vô cùng mạnh mẽ. (Chuẩn bị cho sáo ngữ) Như chú Ben đã dạy chúng tôi, "Với sức mạnh to lớn, trách nhiệm lớn lao."
Andrew Falanga

6

Đầu tiên, C không bao giờ kiểm tra giới hạn mảng. Không quan trọng nếu chúng là cục bộ, toàn cầu, tĩnh, tham số, bất cứ điều gì. Kiểm tra giới hạn mảng có nghĩa là xử lý nhiều hơn và C được cho là rất hiệu quả, vì vậy việc kiểm tra giới hạn mảng được thực hiện bởi lập trình viên khi cần.

Thứ hai, có một mẹo giúp cho việc truyền từng giá trị cho một hàm vào một hàm. Cũng có thể trả về giá trị một mảng từ một hàm. Bạn chỉ cần tạo một kiểu dữ liệu mới bằng struct. Ví dụ:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Bạn phải truy cập các yếu tố như thế này: foo.a [1]. Chữ ".a" thêm có thể trông lạ, nhưng thủ thuật này thêm chức năng tuyệt vời cho ngôn ngữ C.


7
Bạn đang nhầm lẫn kiểm tra giới hạn thời gian chạy với kiểm tra kiểu thời gian biên dịch.
Ben Voigt

@Ben Voigt: Tôi chỉ nói về kiểm tra giới hạn, như câu hỏi ban đầu.
dùng34814

2
@ user34814 kiểm tra giới hạn thời gian biên dịch nằm trong phạm vi kiểm tra loại. Một số ngôn ngữ cấp cao cung cấp tính năng này.
Leushenko

5

Để báo cho trình biên dịch rằng myArray trỏ đến một mảng có ít nhất 10 ints:

void bar(int myArray[static 10])

Một trình biên dịch tốt sẽ đưa ra cảnh báo nếu bạn truy cập myArray [10]. Nếu không có từ khóa "tĩnh", số 10 sẽ không có nghĩa gì cả.


1
Tại sao trình biên dịch nên cảnh báo nếu bạn truy cập phần tử thứ 11 và mảng chứa ít nhất 10 phần tử?
nwellnhof

Có lẽ điều này là do trình biên dịch chỉ có thể thực thi rằng bạn có ít nhất 10 phần tử. Nếu bạn cố gắng truy cập phần tử thứ 11, không thể chắc chắn rằng nó tồn tại (mặc dù có thể).
Dylan Watson

2
Tôi không nghĩ đó là một cách đọc đúng về tiêu chuẩn. [static]cho phép trình biên dịch cảnh báo nếu bạn gọi bar bằng int[5]. Nó không ra lệnh những gì bạn có thể truy cập trong bar . Các onus hoàn toàn ở phía người gọi.
tab

3
error: expected primary-expression before 'static'không bao giờ thấy cú pháp này. điều này không chắc là chuẩn C hay C ++.
v.oddou

3
@ v.oddou, được chỉ định trong C99, trong 6.7.5.2 và 6.7.5.3.
Phường Samuel Edwin

5

Đây là một "tính năng" nổi tiếng của C, được chuyển qua C ++ vì C ++ được cho là biên dịch chính xác mã C.

Vấn đề phát sinh từ một số khía cạnh:

  1. Một tên mảng được cho là hoàn toàn tương đương với một con trỏ.
  2. C được cho là nhanh, ban đầu nhà phát triển là một loại "Trình biên dịch cấp cao" (đặc biệt được thiết kế để viết "Hệ điều hành di động" đầu tiên: Unix), vì vậy nó là không phải nên chèn mã "ẩn"; Do đó, kiểm tra phạm vi thời gian chạy là "bị cấm".
  3. Mã máy được tạo ra để truy cập vào một mảng tĩnh hoặc một mảng động (trong ngăn xếp hoặc được phân bổ) thực sự khác nhau.
  4. Vì hàm được gọi không thể biết "loại" của mảng được truyền dưới dạng đối số, mọi thứ được coi là một con trỏ và được xử lý như vậy.

Bạn có thể nói mảng không thực sự được hỗ trợ trong C (điều này không thực sự đúng, như tôi đã nói trước đây, nhưng đó là một xấp xỉ tốt); một mảng thực sự được coi là một con trỏ tới một khối dữ liệu và được truy cập bằng số học con trỏ. Vì C KHÔNG có bất kỳ dạng RTTI nào, bạn phải khai báo kích thước của phần tử mảng trong nguyên mẫu hàm (để hỗ trợ số học con trỏ). Điều này thậm chí còn "đúng hơn" đối với các mảng đa chiều.

Dù sao tất cả những điều trên không thực sự đúng nữa: p

Hầu hết các trình biên dịch C / C ++ hiện đại đều hỗ trợ kiểm tra giới hạn, nhưng các tiêu chuẩn yêu cầu tắt theo mặc định (để tương thích ngược). Các phiên bản hợp lý gần đây của gcc, ví dụ, thực hiện kiểm tra phạm vi thời gian biên dịch với "-O3 -Wall -Wextra" và kiểm tra giới hạn thời gian chạy đầy đủ với "kiểm tra giới hạn".


Có lẽ C ++ được cho là biên dịch mã C 20 năm trước, nhưng chắc chắn không, và có không trong một thời gian dài (C ++ 98? C99 ít nhất, mà chưa được "cố định" bởi bất kỳ C mới hơn ++ tiêu chuẩn).
hyde

@hyde Nghe có vẻ hơi quá khắc nghiệt với tôi. Để trích dẫn Stroustrup "Với các ngoại lệ nhỏ, C là tập con của C ++." (Bản C ++ PL 4th ed., Giây 1.2.1). Mặc dù cả C ++ và C đều phát triển hơn nữa và các tính năng từ phiên bản C mới nhất tồn tại không có trong phiên bản C ++ mới nhất, nhưng về tổng thể tôi nghĩ rằng trích dẫn Stroustrup vẫn còn hiệu lực.
mvw

@mvw Hầu hết mã C được viết trong millenium này, không cố ý giữ C ++ tương thích bằng cách tránh các tính năng không tương thích, sẽ sử dụng cú pháp khởi tạo được chỉ định C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) để khởi tạo cấu trúc, bởi vì nó chỉ là cách rõ ràng hơn để khởi tạo cấu trúc. Kết quả là, hầu hết mã C hiện tại sẽ bị từ chối bởi trình biên dịch C ++ tiêu chuẩn, bởi vì hầu hết mã C sẽ được khởi tạo cấu trúc.
hyde

@mvw Có lẽ có thể nói, C ++ được cho là tương thích với C vì vậy, có thể viết mã sẽ biên dịch với cả trình biên dịch C và C ++, nếu một số thỏa hiệp nhất định được thực hiện. Nhưng điều đó đòi hỏi phải sử dụng một tập hợp con của cả C và C ++, không chỉ là tập hợp con của C ++.
hyde

@hyde Bạn sẽ ngạc nhiên về số lượng mã C có thể biên dịch được. Một vài năm trước, toàn bộ nhân Linux là C ++ có thể biên dịch được (tôi không biết liệu nó có còn đúng không). Tôi thường xuyên biên dịch mã C trong trình biên dịch C ++ để kiểm tra cảnh báo ưu việt, chỉ có "sản xuất" được biên dịch ở chế độ C để đạt được tối ưu hóa nhất.
ZioByte

3

C sẽ không chỉ biến đổi một tham số loại int[5]thành *int; cho việc kê khai typedef int intArray5[5];, nó sẽ biến đổi một tham số kiểu intArray5để *intlà tốt. Có một số tình huống trong đó hành vi này, mặc dù kỳ lạ, rất hữu ích (đặc biệt là với những thứ như va_listđược định nghĩa trong stdargs.hđó, một số triển khai xác định là một mảng). Sẽ là phi logic khi cho phép như một tham số một loại được xác định là int[5](bỏ qua kích thước) nhưng không cho phépint[5] được chỉ định trực tiếp.

Tôi thấy việc xử lý các tham số của kiểu mảng là vô lý, nhưng đó là kết quả của những nỗ lực để sử dụng ngôn ngữ đặc biệt, phần lớn trong số đó không được xác định rõ hoặc suy nghĩ kỹ và cố gắng đưa ra hành vi thông số kỹ thuật phù hợp với những gì triển khai hiện có đã làm cho các chương trình hiện có. Nhiều điều kỳ quặc của C có ý nghĩa khi được nhìn dưới ánh sáng đó, đặc biệt nếu người ta cho rằng khi nhiều trong số chúng được phát minh, phần lớn ngôn ngữ mà chúng ta biết ngày nay vẫn chưa tồn tại. Theo những gì tôi hiểu, trong tiền thân của C, được gọi là BCPL, trình biên dịch đã không thực sự theo dõi các loại biến rất tốt. Một tuyên bố int arr[5];tương đương với int anonymousAllocation[5],*arr = anonymousAllocation;; một khi phân bổ được đặt sang một bên. trình biên dịch không biết cũng không quan tâmarrlà một con trỏ hoặc một mảng. Khi được truy cập dưới dạng arr[x]hoặc *arr, nó sẽ được coi là một con trỏ bất kể nó được khai báo như thế nào.


1

Một điều chưa được trả lời là câu hỏi thực tế.

Các câu trả lời đã được đưa ra giải thích rằng các mảng không thể được truyền theo giá trị cho hàm trong C hoặc C ++. Họ cũng giải thích rằng một tham số được khai báo như int[]được xử lý như thể nó có kiểu int *và một biến kiểu int[]có thể được truyền cho hàm như vậy.

Nhưng họ không giải thích lý do tại sao nó chưa bao giờ bị lỗi khi cung cấp độ dài mảng một cách rõ ràng.

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

Tại sao không phải là lỗi cuối cùng?

Một lý do cho điều đó là nó gây ra vấn đề với typedefs.

typedef int myarray[10];
void f(myarray array);

Nếu đó là một lỗi khi chỉ định độ dài mảng trong các tham số hàm, bạn sẽ không thể sử dụng myarraytên trong tham số hàm. Và vì một số triển khai sử dụng các kiểu mảng cho các loại thư viện tiêu chuẩn như va_list, và tất cả các cài đặt được yêu cầu để tạo jmp_bufmột kiểu mảng, sẽ rất khó khăn nếu không có cách khai báo các tham số hàm tiêu chuẩn bằng các tên đó: không có khả năng đó, có thể không được thực hiện di động của các chức năng như vprintf.


0

Nó cho phép trình biên dịch có thể kiểm tra xem kích thước của mảng được truyền có giống như những gì được mong đợi hay không. Trình biên dịch có thể cảnh báo một vấn đề nếu không phải như vậy.

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.