2015-10-22 16 views
15

[Đây là câu hỏi được lấy cảm hứng từ một cuộc thảo luận gần đây ở nơi khác và tôi sẽ cung cấp câu trả lời đúng với nó.]Tại sao các mảng trong phân rã C thành con trỏ?

Tôi đã tự hỏi về hiện tượng lẻ của mảng "phân rã" thành con trỏ, ví dụ: khi được sử dụng làm đối số hàm. Điều đó dường như không an toàn. Nó cũng là bất tiện để vượt qua chiều dài một cách rõ ràng với nó. Và tôi có thể vượt qua loại tổng hợp khác - cấu trúc - hoàn toàn tốt theo giá trị; cấu trúc không phân rã.

Lý do cơ bản đằng sau quyết định thiết kế này là gì? Nó tích hợp với ngôn ngữ như thế nào? Tại sao có sự khác biệt với cấu trúc?

+4

Nó tiết kiệm được rất nhiều bản sao (không cần thiết). C được thiết kế cho tốc độ, không an toàn. – Kninnug

+1

Vì sao chép các mảng được truyền dưới dạng đối số sẽ tốn kém và không cần thiết hầu hết thời gian. Ngoài ra, C ban đầu không hỗ trợ các cấu trúc truyền như các đối số hàm, do đó không có lựa chọn thiết kế rõ ràng nào để tạo các mảng khác với các cấu trúc. –

+2

@Kninnug Người ta có thể, tất nhiên, vẫn vượt qua địa chỉ nếu muốn, như với bất cứ điều gì khác. –

Trả lời

16

Hãy kiểm tra chức năng gọi vì vấn đề này là độc đáo có thể nhìn thấy ở đó: Tại sao các mảng không chỉ đơn giản thông qua chức năng như mảng, bởi giá trị, như một bản sao?

Có một lý do hoàn toàn thực dụng đầu tiên: Mảng có thể lớn; nó có thể không được khuyến khích để vượt qua chúng theo giá trị bởi vì họ có thể vượt quá kích thước ngăn xếp, đặc biệt là trong những năm 1970. Các trình biên dịch đầu tiên được viết trên PDP-7 với RAM 9 kB.

Ngoài ra còn có một lý do kỹ thuật khác bắt nguồn từ ngôn ngữ. Sẽ rất khó để tạo mã cho một cuộc gọi hàm với các đối số có kích thước không được biết tại thời gian biên dịch. Đối với tất cả các mảng, bao gồm các mảng độ dài biến đổi trong C hiện đại, chỉ cần các địa chỉ được đặt trên ngăn xếp cuộc gọi. Kích thước của một địa chỉ là tất nhiên được biết đến nhiều. Ngay cả các ngôn ngữ với các loại mảng phức tạp mang thông tin kích thước thời gian chạy không vượt qua các đối tượng thích hợp trên ngăn xếp. Những ngôn ngữ này thường vượt qua "xử lý" xung quanh, đó là những gì C đã thực hiện hiệu quả, quá, trong 40 năm. Xem Jon Skeet here và một lời giải thích minh họa ông tham khảo (sic) here.

Bây giờ, một ngôn ngữ có thể làm cho nó trở thành một yêu cầu mà một mảng luôn có một kiểu hoàn chỉnh; tức là bất cứ khi nào nó được sử dụng, việc khai báo hoàn chỉnh của nó bao gồm cả kích thước phải được hiển thị. Đây là, sau khi tất cả, những gì C yêu cầu từ các cấu trúc (khi chúng được truy cập). Do đó, các cấu trúc có thể được truyền cho các hàm theo giá trị. Yêu cầu loại hoàn chỉnh cho mảng cũng sẽ làm cho các cuộc gọi chức năng dễ dàng compilable và obviate sự cần thiết phải vượt qua đối số chiều dài bổ sung: sizeof() vẫn sẽ làm việc như mong đợi bên trong callee. Nhưng hãy tưởng tượng điều đó có nghĩa là gì. Nếu kích thước thật sự là một phần của kiểu lập luận của mảng, chúng ta sẽ cần một chức năng riêng biệt cho mỗi kích thước mảng:

// for user input. 
int average_Ten(int arr[10]); 

// for my new Hasselblad. 
int average_ThreeTrillionThreehundredninetythreeBillionNinehundredtwentyeightMillionEighthundredsixthousandfourhundred(int arr[16544*12400]); 
// ... 

Trong thực tế nó sẽ là hoàn toàn tương đương với cấu trúc trôi qua, mà khác nhau về loại nếu các yếu tố của họ khác nhau (nói, một cấu trúc với 10 phần tử int và một với 16544 * 12400). Rõ ràng là mảng cần linh hoạt hơn. Ví dụ, như đã chứng minh một cách không thể cung cấp một cách hợp lý các chức năng thư viện có thể sử dụng chung có tham số mảng.

"Câu hỏi hóc búa mạnh mẽ" này, trên thực tế, điều xảy ra trong C++ khi một hàm tham chiếu đến một mảng; đó cũng là lý do tại sao không ai làm điều đó, ít nhất là không rõ ràng. Nó hoàn toàn bất tiện đến mức vô ích ngoại trừ trường hợp nhắm mục tiêu sử dụng cụ thể, và trong mã chung: C++ mẫu cung cấp tính linh hoạt thời gian biên dịch mà không có sẵn trong C.

Nếu, trong C hiện tại, thực sự mảng kích thước được biết đến nên được truyền theo giá trị luôn luôn có khả năng quấn chúng trong một cấu trúc. Tôi nhớ rằng một số tiêu đề liên quan đến IP trên Solaris xác định cấu trúc gia đình địa chỉ với mảng trong chúng, cho phép sao chép chúng xung quanh.Bởi vì bố trí byte của cấu trúc đã được cố định và đã biết, điều đó có ý nghĩa.

Đối với một số nền, cũng thú vị khi đọc Sự phát triển ngôn ngữ C của Dennis Ritchie về nguồn gốc của C. C tiền thân BCPL không có bất kỳ mảng nào; bộ nhớ chỉ là bộ nhớ tuyến tính thuần nhất với con trỏ vào nó.

+0

Câu trả lời hay. Tất cả những gì tôi muốn nói thêm là nó có ý nghĩa đối với các mảng để "phân hủy" thành con trỏ bởi vì chúng cơ bản giống nhau ... tức là. chỉ là một cách để lặp qua các byte 'sizeof (int)' trong bộ nhớ bắt đầu từ một địa chỉ đã cho. – DIMMSum

+1

Sử dụng thú vị tín hiệu * Cf. *, có thể * Accord *, * Xem * hoặc * Xem thêm * có thể thích hợp hơn một chút tùy thuộc vào sự hỗ trợ trực tiếp cho vay bởi liên kết của Skeet. * Cf * chỉ ra * khác biệt * hoặc * khởi hành * từ điểm chính nhưng đủ hỗ trợ tương tự để đảm bảo so sánh. –

+1

Haha, yêu ví dụ của bạn. – Himself12794

0

Mang máy thời gian của bạn và quay lại năm 1970. Bắt đầu thiết kế ngôn ngữ lập trình. Bạn muốn mã sau để biên dịch và thực hiện điều mong đợi:

size_t i; 
int* p = (int *) malloc (10 * sizeof (int)); 
for (i = 0; i < 10; ++i) p [i] = i; 

int a [10]; 
for (i = 0; i < 10; ++i) a [i] = i; 

Đồng thời, bạn muốn ngôn ngữ đơn giản. Đơn giản, đủ để bạn có thể biên dịch nó trên máy tính của năm 1970. Quy tắc "a" phân rã thành "con trỏ thành phần tử đầu tiên của một" đạt được điều đó một cách độc đáo.

+3

Các ngôn ngữ khác có thể thực hiện tại thời điểm đó (Algol68). Toàn bộ ý tưởng về "thiết kế" có vẻ hơi lệch, nếu bạn đọc giấy của Ritchie - đó là tiến hóa hơn ;-) –

10

Câu trả lời cho câu hỏi này có thể được tìm thấy trong giấy "The Development of the C Language" Dennis Ritchie (xem "phôi C" phần)

Theo Dennis Ritchie, các phiên bản mới ra đời của C trực tiếp kế thừa/ngữ nghĩa mảng nuôi từ B và BCPL ngôn ngữ - người tiền nhiệm của C. Trong các mảng ngôn ngữ được thực hiện theo nghĩa đen là con trỏ vật lý. Các con trỏ này chỉ các khối bộ nhớ được phân bổ độc lập có chứa các phần tử mảng thực tế. Những con trỏ được khởi tạo vào thời gian chạy. I E. trong mảng B và BCPL ngày được thực hiện dưới dạng đối tượng "nhị phân" (hai bên): một con trỏ độc lập trỏ đến khối dữ liệu độc lập. Không có sự khác biệt giữa ngữ nghĩa con trỏ và mảng trong các ngôn ngữ đó, ngoài thực tế là các con trỏ mảng được khởi tạo tự động. Bất cứ lúc nào nó có thể gán lại một con trỏ mảng trong B và BCPL để làm cho nó trỏ đến một nơi khác.

Ban đầu, phương pháp tiếp cận ngữ nghĩa mảng này được thừa kế bởi C. Tuy nhiên, nhược điểm của nó trở nên rõ ràng ngay lập tức khi struct loại được đưa vào ngôn ngữ (thứ gì đó không phải B cũng như BCPL). Và ý tưởng là các cấu trúc nên tự nhiên có thể chứa mảng. Tuy nhiên, việc tiếp tục gắn bó với tính chất "lưỡng cực" ở trên của các mảng B/BCPL ngay lập tức sẽ dẫn đến một số biến chứng rõ ràng với các cấu trúc. Ví dụ. các đối tượng struct với các mảng bên trong sẽ yêu cầu "xây dựng" không tầm thường ở điểm định nghĩa. Nó sẽ trở thành không thể sao chép các đối tượng cấu trúc như vậy - một cuộc gọi memcpy thô sẽ sao chép các con trỏ mảng mà không sao chép dữ liệu thực tế. Người ta sẽ không thể malloc đối tượng struct, vì malloc chỉ có thể cấp phát bộ nhớ thô và không kích hoạt bất kỳ khởi tạo không tầm thường nào. Vân vân và vân vân.

Điều này được coi là không thể chấp nhận, dẫn đến việc thiết kế lại các mảng C. Thay vì thực hiện các mảng thông qua các con trỏ vật lý, Ritchie đã quyết định loại bỏ hoàn toàn các con trỏ. Mảng mới được thực hiện như một khối bộ nhớ ngay lập tức, đó là chính xác những gì chúng ta có trong C ngày hôm nay. Tuy nhiên, vì các lý do tương thích ngược, hành vi của mảng B/BCPL được bảo toàn (mô phỏng) càng nhiều càng tốt ở mức bề ngoài: mảng C mới dễ dàng bị phân rã thành giá trị con trỏ tạm thời, trỏ tới phần đầu của mảng. Phần còn lại của chức năng mảng vẫn không thay đổi, dựa vào kết quả có sẵn của phân rã.

Để báo giấy nói trên

Các giải pháp lập nhảy quan trọng trong chuỗi tiến hóa giữa typeless BCPL và gõ C. Nó loại bỏ việc thực của con trỏ trong lưu trữ, và thay vào đó gây ra việc tạo ra con trỏ khi tên mảng được đề cập trong một biểu thức.Quy tắc, tồn tại trong C ngày nay, là giá trị của loại mảng là được chuyển đổi, khi chúng xuất hiện trong biểu thức, thành con trỏ đến số đầu tiên của các đối tượng tạo mảng.

Sáng chế này đã bật hầu hết mã B hiện có để tiếp tục hoạt động, bất chấp sự thay đổi cơ bản trong ngữ nghĩa của ngôn ngữ. Vài chương trình đã gán giá trị mới cho tên mảng để điều chỉnh nguồn gốc của nó — có thể trong B và BCPL, vô nghĩa trong C — đã được sửa chữa dễ dàng. Quan trọng hơn, ngôn ngữ mới giữ lại sự giải thích rõ ràng và khả thi (nếu khác thường) về ngữ nghĩa của mảng, trong khi mở đường thành cấu trúc kiểu toàn diện hơn.

Vì vậy, câu trả lời trực tiếp cho bạn "tại sao" Câu hỏi như sau: mảng trong C được thiết kế để phân hủy để con trỏ để thi đua (càng gần càng tốt) các hành vi lịch sử của mảng trong B và Ngôn ngữ BCPL.

+0

Thông tin rất tốt. Tôi nghĩ chỉ bây giờ tôi hiểu đoạn văn bạn trích dẫn hoàn toàn. –

Các vấn đề liên quan