Tại sao cú pháp C cho mảng, con trỏ và hàm được thiết kế theo cách này?


16

Sau khi thấy (và hỏi!) Rất nhiều câu hỏi tương tự như

Không gì int (*f)(int (*a)[5])có nghĩa là trong C?

và thậm chí khi thấy rằng họ đã tạo ra một chương trình để giúp mọi người hiểu cú pháp C, tôi không thể không tự hỏi:

Tại sao cú pháp của C được thiết kế theo cách này?

Ví dụ, nếu tôi đang thiết kế các con trỏ, tôi sẽ dịch "một con trỏ thành một mảng gồm 10 phần tử con trỏ" thành

int*[10]* p;

không

int* (*p)[10];

mà tôi cảm thấy hầu hết mọi người sẽ đồng ý là ít đơn giản hơn nhiều.

Vì vậy, tôi tự hỏi, tại sao, cú pháp, không trực quan? Có một vấn đề cụ thể mà cú pháp giải quyết (có lẽ là một sự mơ hồ?) Mà tôi không biết?


2
Bạn biết không có câu trả lời thực sự cho điều này, và những câu hỏi như vậy. Đúng? Những gì bạn sẽ nhận được chỉ là phỏng đoán.
BЈовић

7
@VJo - cũng có thể có câu trả lời "thực tế" (nghĩa là khách quan) - các tác giả ngôn ngữ và ủy ban tiêu chuẩn cũng đã biện minh rõ ràng (hoặc ít nhất là giải thích) nhiều quyết định này.
gièm pha

Tôi không nghĩ cú pháp đề xuất của bạn nhất thiết phải nhiều hơn hoặc ít "trực quan" hơn cú pháp C. C là những gì nó là; một khi bạn đã học nó, bạn sẽ không bao giờ có những câu hỏi này nữa. Nếu bạn chưa học nó ... tốt, có lẽ đó là vấn đề thực sự.
Caleb

1
@Caleb: Thật buồn cười là bạn đã kết luận điều đó một cách dễ dàng như thế nào, bởi vì tôi đã học được nó và tôi vẫn có câu hỏi này ...
Mehrdad

1
Các cdecllệnh là rất tiện dụng để giải mã tờ khai C phức tạp. Ngoài ra còn có một giao diện web tại cdecl.org .
Keith Thompson

Câu trả lời:


16

Sự hiểu biết của tôi về lịch sử của nó là nó dựa trên hai điểm chính ...

Đầu tiên, các tác giả ngôn ngữ ưa thích làm cho cú pháp biến thành trung tâm thay vì kiểu trung tâm. Nghĩa là, họ muốn có một lập trình viên để nhìn vào tờ khai và nghĩ "nếu tôi viết biểu thức *func(arg), mà sẽ dẫn đến int, nếu tôi viết *arg[N]tôi sẽ có một phao" hơn là " funcphải là một con trỏ tới một hàm dùng này và trả lại điều đó ".

Mục C trên Wikipedia tuyên bố rằng:

Ý tưởng của Ritchie là khai báo các định danh trong các ngữ cảnh giống như cách sử dụng của họ: "khai báo phản ánh việc sử dụng".

... trích dẫn p122 của K & R2, tuy nhiên, tôi không phải ra tay để tìm báo giá mở rộng cho bạn.

Thứ hai, thực sự rất khó để đưa ra một cú pháp khai báo phù hợp khi bạn đang xử lý các mức độ tùy tiện. Ví dụ của bạn có thể hoạt động tốt để thể hiện loại bạn nghĩ ra từ đó, nhưng nó có mở rộng thành một hàm lấy một con trỏ tới một mảng các loại đó và trả lại một số mớ hỗn độn đáng ghét khác không? (Có thể là như vậy, nhưng bạn đã kiểm tra chưa? Bạn có thể chứng minh không? ).

Hãy nhớ rằng, một phần thành công của C là do các trình biên dịch được viết cho nhiều nền tảng khác nhau, và vì vậy tốt hơn là nên bỏ qua một số mức độ dễ đọc vì mục đích làm cho trình biên dịch dễ viết hơn.

Phải nói rằng, tôi không phải là một chuyên gia về ngữ pháp ngôn ngữ hoặc trình biên dịch. Nhưng tôi biết đủ để biết có rất nhiều điều cần biết;)


2
"Làm cho trình biên dịch dễ viết hơn" ... ngoại trừ C nổi tiếng là khó phân tích (chỉ đứng đầu bởi C ++).
Jan Hudec

1
@JanHudec - À ... ừ. Đó không phải là một tuyên bố kín nước. Nhưng trong khi C không thể phân tích cú pháp như một ngữ pháp không ngữ cảnh, một khi một người đã nghĩ ra cách phân tích nó, thì đây không còn là bước khó khăn nữa. Và thực tế là, nó nhiều trong những ngày đầu của nó do người có khả năng bang ra trình biên dịch một cách dễ dàng, vì vậy K & R phải đã xảy ra một số sự cân bằng. (Trong Richard Gabriel khét tiếng The Rise of "Tệ hơn là tốt hơn" , ông cho là hiển nhiên - và bemoans -. Thực tế là nó dễ dàng để viết một trình biên dịch C cho một nền tảng mới)
detly

Nhân tiện, tôi rất vui khi được sửa chữa điều này - tôi không biết nhiều về phân tích cú pháp và ngữ pháp. Tôi sẽ đi sâu hơn vào suy luận từ thực tế lịch sử.
gièm pha

12

Rất nhiều điều kỳ lạ của ngôn ngữ C có thể được giải thích bằng cách máy tính làm việc khi nó được thiết kế. Số lượng bộ nhớ lưu trữ rất hạn chế, vì vậy việc giảm thiểu kích thước của các tệp mã nguồn là rất quan trọng . Việc thực hành lập trình từ những năm 70 và 80 là để đảm bảo mã nguồn chứa càng ít ký tự càng tốt và tốt nhất là không có nhận xét mã nguồn quá mức.

Điều này tất nhiên là vô lý ngày nay, với khá nhiều không gian lưu trữ không giới hạn trên các ổ đĩa cứng. Nhưng nó là một phần lý do tại sao C có cú pháp kỳ lạ như vậy nói chung.


Về con trỏ mảng cụ thể, ví dụ thứ hai của bạn phải là int (*p)[10];(vâng cú pháp rất khó hiểu). Có lẽ tôi sẽ đọc nó là "int con trỏ đến mảng mười" ... điều này có ý nghĩa phần nào. Nếu không phải là dấu ngoặc đơn, trình biên dịch sẽ diễn giải nó như là một mảng gồm mười con trỏ, điều này sẽ mang lại cho khai báo một ý nghĩa hoàn toàn khác.

Vì các con trỏ mảng và các con trỏ hàm đều có cú pháp khá tối nghĩa trong C, nên điều hợp lý để làm là loại bỏ sự kỳ lạ. Có lẽ như thế này:

Ví dụ tối nghĩa:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Không tối nghĩa, ví dụ tương đương:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

Mọi thứ thậm chí còn khó hiểu hơn nếu bạn đang xử lý các mảng con trỏ hàm. Hoặc tối nghĩa nhất của tất cả chúng: các hàm trả về hàm con trỏ (hữu ích nhẹ). Nếu bạn không sử dụng typedefs cho những thứ đó, bạn sẽ nhanh chóng phát điên.


Ah, cuối cùng là một câu trả lời hợp lý. :-) Tôi tò mò về cách cú pháp cụ thể thực sự thu nhỏ kích thước mã nguồn, nhưng dù sao đó cũng là một ý tưởng hợp lý và có ý nghĩa. Cảm ơn. +1
Mehrdad

Tôi muốn nói rằng nó ít hơn về kích thước mã nguồn và nhiều hơn về cách viết trình biên dịch, nhưng chắc chắn +1 cho "typdef đi sự kỳ lạ". Sức khỏe tinh thần của tôi cải thiện đáng kể vào ngày tôi nhận ra mình có thể làm điều này.
gièm pha

2
[Cần dẫn nguồn] về điều kích thước mã nguồn. Tôi chưa bao giờ nghe về một giới hạn như vậy (mặc dù có lẽ đó là điều "mọi người đều biết").
Sean McMillan

1
Vâng, tôi đã mã hóa các chương trình trong thập niên 70 trong COBOL, Trình biên dịch, CORAL và PL / 1 trên bộ công cụ IBM, DEC và XEROX và tôi chưa bao giờ gặp phải giới hạn kích thước mã nguồn. Hạn chế về kích thước mảng, kích thước thực thi, kích thước tên chương trình - nhưng không bao giờ kích thước mã nguồn.
James Anderson

1
@Sean McMillan: Tôi không nghĩ kích thước mã nguồn là một hạn chế (xem xét rằng tại thời điểm đó, các ngôn ngữ dài dòng như Pascal khá phổ biến). Và ngay cả khi đây là trường hợp, tôi nghĩ rằng sẽ rất dễ dàng phân tích trước mã nguồn và thay thế các từ khóa dài bằng các mã một byte ngắn (ví dụ như một số trình thông dịch cơ bản được sử dụng). Vì vậy, tôi thấy đối số "C là ngắn gọn vì nó được phát minh trong thời kỳ có ít bộ nhớ hơn" hơi yếu.
Giorgio

7

Nó khá đơn giản: int *pcó nghĩa *plà một int; int a[5]có nghĩa a[i]là một int.

int (*f)(int (*a)[5])

Có nghĩa *flà một hàm, *alà một mảng gồm năm số nguyên, do đó, fmột hàm lấy một con trỏ tới một mảng gồm năm số nguyên và trả về int. Tuy nhiên, trong C, việc truyền một con trỏ tới một mảng là không hữu ích.

C khai báo rất hiếm khi có được phức tạp này.

Ngoài ra, bạn có thể làm rõ bằng cách sử dụng typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);

4
Xin lỗi nếu điều này nghe có vẻ thô lỗ (ý tôi không phải vậy), nhưng tôi nghĩ rằng bạn đã bỏ lỡ toàn bộ điểm của câu hỏi ...: \
Mehrdad

2
@Mehrdad: Tôi không thể nói cho bạn biết những gì trong tâm trí của Kernighan và Ritchie; Tôi đã nói với bạn logic đằng sau cú pháp. Tôi không biết về hầu hết mọi người, nhưng tôi không nghĩ rằng cú pháp được đề xuất của bạn rõ ràng hơn.
kevin cline

Tôi đồng ý - thật bất thường khi thấy một tuyên bố phức tạp như vậy.
Caleb

Thiết kế của C tờ khai có trước typedef, const, volatile, và khả năng để khởi tạo điều trong tờ khai. Nhiều sự mơ hồ khó chịu của cú pháp khai báo (ví dụ: int const *p, *q;nên liên kết constvới loại hoặc khai báo) không thể phát sinh trong ngôn ngữ như thiết kế ban đầu. Tôi muốn ngôn ngữ đã thêm dấu hai chấm giữa loại và khai báo, nhưng cho phép bỏ qua nó khi sử dụng các loại "từ dành riêng" tích hợp mà không cần vòng loại. Ý nghĩa của int: const *p,*q;int const *: p,*q;sẽ rõ ràng.
supercat

3

Tôi nghĩ bạn phải coi * [] là các toán tử được gắn vào một biến. * được viết trước một biến, [] sau.

Hãy đọc biểu thức

int* (*p)[10];

Phần tử trong cùng là p, một biến, do đó

p

có nghĩa là: p là một biến.

Trước biến có một dấu *, toán tử * luôn được đặt trước biểu thức mà nó đề cập đến, do đó,

(*p)

có nghĩa là: biến p là một con trỏ. Không có toán tử () toán tử [] ở bên phải sẽ có quyền ưu tiên cao hơn, nghĩa là

**p[]

sẽ được phân tích thành

*(*(p[]))

Bước tiếp theo là []: vì không có thêm (), [] có quyền ưu tiên cao hơn bên ngoài *, do đó

(*p)[]

có nghĩa là: (biến p là một con trỏ) đến một mảng. Sau đó, chúng tôi có thứ hai *:

* (*p)[]

có nghĩa là: ((biến p là một con trỏ) đến một mảng) của các con trỏ

Cuối cùng, bạn có toán tử int (tên loại), có mức ưu tiên thấp nhất:

int* (*p)[]

nghĩa là: (((biến p là con trỏ) đến một mảng) của con trỏ) thành số nguyên.

Vì vậy, toàn bộ hệ thống dựa trên các biểu thức kiểu với các toán tử và mỗi toán tử có các quy tắc ưu tiên riêng. Điều này cho phép xác định các loại rất phức tạp.


0

Không quá khó khi bạn bắt đầu suy nghĩ và C không bao giờ là ngôn ngữ rất dễ dàng. Và int*[10]* pthực sự không dễ hơn int* (*p)[10] Và loại k sẽ ở trongint*[10]* p, k;


2
k sẽ là một đánh giá mã thất bại, tôi có thể tìm ra trình biên dịch sẽ làm gì, tôi thậm chí có thể bị làm phiền, nhưng tôi không thể tìm ra những gì lập trình viên dự định - thất bại ............
mattnz

và tại sao k sẽ thất bại xem xét mã?
Dainius

1
bởi vì mã không thể đọc được và không thể nhầm lẫn. Mã không đúng để sửa, rõ ràng là đúng và có khả năng vẫn đúng mặc dù bảo trì. Thực tế bạn phải hỏi loại k sẽ là một dấu hiệu mã không thể đáp ứng các yêu cầu cơ bản này.
mattnz

1
Về cơ bản có 3 khai báo biến (trong trường hợp này) thuộc các loại khác nhau trên cùng một hàng, ví dụ int * p, int i [10] và int k. Điều đó là không thể chấp nhận được. Nhiều khai báo cùng loại được chấp nhận, miễn là các biến có một số dạng quan hệ, ví dụ int width, height, height; Hãy ghi nhớ nhiều người lập trình bằng cách sử dụng int * p, vì vậy tôi đang ở 'int * p, i;'.
mattnz

1
Điều @mattnz đang cố gắng nói là bạn có thể thông minh như bạn muốn, nhưng tất cả đều vô nghĩa khi ý định của bạn không rõ ràng và / hoặc mã của bạn được viết kém / không thể đọc được. Loại công cụ này thường dẫn đến mã bị hỏng và lãng phí thời gian. Thêm vào đó, pointer to intintthậm chí không cùng loại, vì vậy chúng nên được khai báo riêng. Giai đoạn = Stage. Lắng nghe người đàn ông. Anh ấy có đại diện 18k vì một lý do.
Braden hay nhất
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.