簡體   English   中英

為什么 C 中的數組會衰減為指針?

[英]Why do arrays in C decay to pointers?

[這是一個受到最近在別處討論的啟發的問題,我將提供一個正確的答案。]

我想知道數組“衰減”到指針的奇怪 C 現象,例如當用作函數參數時。 這似乎太不安全了。 用它顯式傳遞長度也很不方便。 可以通過值很好地傳遞另一種類型的聚合——結構體; 結構不會衰減。

這個設計決定背后的基本原理是什么? 它如何與語言相結合? 為什么結構有區別?

基本原理

讓我們檢查函數調用,因為那里的問題很明顯:為什么數組不是簡單地作為數組、按值、作為副本傳遞給函數?

首先有一個純粹實用的原因:數組可以很大; 可能不建議按值傳遞它們,因為它們可能會超過堆棧大小,尤其是在 1970 年代。 第一個編譯器是在具有大約 9 kB RAM 的 PDP-7 上編寫的。

語言中還有一個更技術性的原因。 很難為參數大小在編譯時未知的函數調用生成代碼。 對於所有數組,包括現代 C 中的可變長度數組,只需將地址放在調用堆棧上。 地址的大小當然是眾所周知的。 即使具有攜帶運行時大小信息的復雜數組類型的語言也不會在堆棧上傳遞正確的對象。 這些語言通常會傳遞“句柄”,這也是 C 40 年來有效完成的工作。 請參閱此處的Jon Skeet 以及他在此處引用的圖解說明(原文如此)。

現在,一種語言可以要求數組始終具有完整類型; 即無論何時使用它,它的完整聲明包括大小必須是可見的。 畢竟,這是 C 對結構的要求(當它們被訪問時)。 因此,結構可以按值傳遞給函數。 要求數組的完整類型也將使函數調用易於編譯並避免傳遞額外長度參數的需要: sizeof()仍將在被調用者內部按預期工作。 但想象一下這意味着什么。 如果大小確實是數組參數類型的一部分,我們需要為每個數組大小設置一個不同的函數:

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

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

事實上,它與傳遞結構完全可比,如果它們的元素不同,則它們的類型不同(例如,一個結構具有 10 個 int 元素,一個結構具有 16544*12400)。 很明顯,數組需要更多的靈活性。 例如,正如所展示的那樣,人們無法明智地提供通常可用的接受數組參數的庫函數。

這種“強類型難題”實際上是 C++ 中函數引用數組時發生的情況; 這也是為什么沒有人這樣做的原因,至少沒有明確地這樣做。 除了針對特定用途和通用代碼的情況外,它完全不方便以至於無用:C++ 模板提供了 C 中沒有的編譯時靈活性。

如果在現有的 C 中,確實應該按值傳遞已知大小的數組,那么總是有可能將它們包裝在一個結構中。 我記得 Solaris 上的一些與 IP 相關的標頭定義了地址族結構,其中包含數組,允許復制它們。 因為結構體的字節布局是固定且已知的,所以這是有道理的。

對於某些背景,閱讀 Dennis Ritchie 撰寫的有關 C 起源的 C 語言的發展也很有趣。C 的前身 BCPL 沒有任何數組; 內存只是帶有指向它的指針的同構線性內存。

這個問題的答案可以在 Dennis Ritchie 的“The Development of the C Language”論文中找到(參見“Embryonic C”部分)

根據 Dennis Ritchie 的說法,C 的新生版本直接從 B 和 BCPL 語言(C 的前身)繼承/采用了數組語義。在這些語言中,數組實際上是作為物理指針實現的。 這些指針指向獨立分配的包含實際數組元素的內存塊。 這些指針在運行時初始化。 即回到 B 和 BCPL 天數組被實現為“二進制”(二分)對象:指向獨立數據塊的獨立指針。 除了數組指針是自動初始化的事實之外,這些語言中的指針和數組語義沒有區別。 在任何時候都可以在 B 和 BCPL 中重新分配數組指針以使其指向其他地方。

最初,這種數組語義的方法被 C 繼承了。然而,當struct類型被引入到語言中時,它的缺點立即變得明顯(B 和 BCPL 都沒有)。 這個想法是結構自然應該能夠包含數組。 然而,繼續堅持 B/BCPL 數組的上述“二分”性質將立即導致結構的許多明顯並發症。 例如,內部帶有數組的結構對象在定義時需要非平凡的“構造”。 復制這樣的結構對象變得不可能——原始的memcpy調用將復制數組指針而不復制實際數據。 不能malloc struct 對象,因為malloc只能分配原始內存並且不會觸發任何非平凡的初始化。 等等等等。

這被認為是不可接受的,這導致了 C 數組的重新設計。 Ritchie 決定完全擺脫指針,而不是通過物理指針實現數組。 新數組被實現為單個立即內存塊,這正是我們今天在 C 中所擁有的。 然而,出於向后兼容性的原因,B/BCPL 數組的行為在表面級別被盡可能多地保留(模擬):新的 C 數組很容易衰減為臨時指針值,指向數組的開頭。 其余的陣列功能保持不變,依賴於衰減的現成結果。

引用上述論文

該解決方案構成了無類型 BCPL 和有類型 C 之間進化鏈中的關鍵跳躍。它消除了存儲中指針的具體化,而是在表達式中提到數組名稱時導致創建指針。 在今天的 C 中仍然存在的規則是,當數組類型的值出現在表達式中時,它們被轉換為指向組成數組的第一個對象的指針。

這項發明使大多數現有的 B 代碼能夠繼續工作,盡管語言的語義發生了潛在的變化。 為數組名稱分配新值以調整其原點的少數程序(在 B 和 BCPL 中可能,在 C 中沒有意義)很容易修復。 更重要的是,新語言保留了對數組語義的連貫且可行(如果不尋常)的解釋,同時為更全面的類型結構開辟了道路。

因此,對您的“為什么”問題的直接回答如下:C 中的數組旨在衰減為指針,以模擬(盡可能接近)B 和 BCPL 語言中數組的歷史行為。

帶上您的時光機,回到 1970 年。開始設計編程語言。 您希望以下代碼編譯並執行預期的操作:

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;

同時,您需要一種簡單的語言。 足夠簡單,您可以在 1970 年代的計算機上編譯它。 “a”衰減為“指向 a 的第一個元素的指針”的規則很好地實現了這一點。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM