首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

數組與指標背後的記憶體真相:何時相同、何時不同、為什麼函數參數會退化

     編輯:關於C語言

回顧:聲明的三種語境

上一篇確立:數組名在「表達式中」退化為指標。但聲明語境下有三種情況:

  1. 外部數組聲明:extern int a[]; → 仍是數組類型
  2. 數組定義:int a[10]; → 分配空間,數組類型
  3. 函數參數聲明: void func(int a[]); → 編譯器強制轉為指標 void func(int *a);

聲明語境與類型轉換關係圖

核心規則:只有在函數參數聲明中,數組才「真正」變成指標

  • 其他所有聲明/定義語境:數組就是數組,指標就是指標,型別嚴格區分。
  • 函數參數聲明語境:int a[]、int a[10]、int *a 完全等價,函數內部拿到的都是指標。

為什麼 C 標準強制函數參數退化?

效率考量。 C 採用「值傳遞」。若數組不退化,每次調用需將整個數組內容複製到棧幀,對大數組極其昂貴。退化為指標後,僅傳遞首地址(4/8 字節),函數直接操作原記憶體。

這也是 C++ 引入「引用傳遞」void func(int (&a)[10]) 的原因之一:保留數組類型資訊(長度),同時避免拷貝。

編譯器視角的統一模型

  • 對編譯器:數組 = 一個地址常量,指標 = 一個地址變數(存放地址的地址)。
  • 表達式 a[i] 統一重寫為 *(a + i)。
  • 指標永遠不會被重寫為數組,但可對指標用下標(前提是指向有效記憶體)。

記憶體訪問微操作對比(偽匯編視角)

數組遍歷 for(i=0;i<10;i++) a[i]=0;

1. 基址 a → R1          (循環外不變)
2. 變數 i 地址 → R2     (循環外不變)
3. 循環體:
   3.1 [R2] → R3        (取 i 的值)
   3.2 R3 * scale → R4  (計算偏移)
   3.3 R1 + R4 → R5     (目標地址)
   3.4 0 → [R5]         (存值)

優勢:基址、i 地址均為編譯期常量,易優化、易預取。

指標遍歷 p=a; for(i=0;i<10;i++) *p++=0;

1. 元素大小 → R5       (循環外)
2. p 地址 → R0         (循環外)
3. 循環體:
   3.1 [R0] → R1        (取 p 當前值 = 目標地址)
   3.2 0 → [R1]         (存值)
   3.3 R1 + R5 → R1     (指標後移)
   3.4 R1 → [R0]        (更新 p)

差異:指標遍歷需額外「讀取指標變數」與「寫回指標變數」,多兩次記憶體訪問(除非寄存器優化)。

可互換性總結表 (Expert C Programming)

場景數組寫法指標寫法是否等價
表達式中訪問 a[i]✓✓ (指標亦可用下標)是,均重寫為 *(a+i)
函數參數聲明void f(int a[])void f(int *a)是,唯一合法互換處
函數調用傳實參傳數組名傳指標是,數組名自動退化為指標
外部聲明 externextern int a[];extern int *a;否! 型別不匹配,鏈接/運行錯誤
定義int a[10];int *a;否! 前者分配 10 個 int,後者僅分配 1 個指標

實戰避坑清單

  1. 跨檔案共享數組: 定義 int a[100];,聲明 必須 extern int a[]; 或 extern int a[100];,絕不可 extern int *a;。
  2. sizeof 陷阱: 函數參數中 sizeof(a) 返回指標大小,而非數組大小。需額外傳長度參數。
  3. 多維數組參數: void f(int a[][COLS]) 第二維必須固定,退化為 int (*a)[COLS](指向陣列的指標),而非 int **。

徹底理解「編譯期地址常量」與「運行期地址變數」的二元本質,配合標準規定的三條退化規則,即可在任何語境下精準判斷數組與指標的行為,寫出零驚喜的 C 代碼。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved