【原創】淺談指針(十三)指向數組的指針

前言

這兩天又在首頁看見指針的文章了,隨手再來寫一篇。本來想先寫static的下集的,後來發現似乎寫的有些問題,草稿已經在寫了,預計後面不久再發佈。
指針其實是C++或是C語言中必不可少的一部分。即使說,很多情況下我們並不會直接使用到指針,但是指針的一些知識同樣在其它的地方(哪怕看似和指針無關的地方)奏效。

1.預備知識的複習

1.1.數組

數組在表達式中,其最高一維會被轉化為指針。對於一維數組,其呈現的形態就是在表達式中轉化為指針。函數的參數也是表達式。

#include<iostream>
using namespace std;
void func(char s[]){
  cout<<sizeof(s)<<endl;
}
int main(){
  char s[]="hello";
  cout<<sizeof(s)<<endl;
  func(s);
}

在64位環境下的輸出:

6
8

1.2.VLA

在C99中,對於非static修飾的局部變量,可以在定義中,數組的元素寫成變量,這一功能叫做可變長數組(Variable Length Array,簡稱VLA)。
下面是一段倒序輸出數組的代碼:

#include<iostream>
using namespace std;

int main(){
    int n;
    cin>>n;
    int s1[n];
    for(int i=0;i<n;i++)cin>>s1[i];
    for(int i=n-1;i>=0;i--)cout<<s1[i]<<" ";
}

其中,”int s1[n]”一句就是運用到了可變長數組。
但在C11中,VLA降為了可選功能,而且VLA只能使用在非static的局部變量,用途相比起來也比較有限。當然,很多情況下還是使用這一功能比較方便。

2.指向數組的指針

指針既然可以指向單個元素,因此也可以指向其他的內容,例如數組。

2.1.指向二維數組

二維數組的類型是int [][],放入表達式中,最高一維會被轉化為指針,也就是int (*)[]。我們就可以使用這樣類型的指針來指向這個二維數組。

#include<iostream>
using namespace std;

int main(){
    int a[2][3]={1,2,3,4,5,6};
    for(int i=0;i<2;i++){
        for(int j=0;j<3;j++){
            cout<<a[i][j]<<" ";
        }
        cout<<endl;
    }
    
    int (*p)[3]=a;
    for(;p!=&a[2];p++){
        for(int j=0;j<3;j++){
            cout<<(*p)[j]<<" ";
        }
        cout<<endl;
    }
}

在代碼的第13行中,聲明了int (*p)[3],它表示「指向元素個數為3的數組的指針」。
之前的文章中提到過網頁鏈接,二維數組在內存中是連續排列的:

如果使用普通的int*指針指向這個數組,每次前進的是sizeof(int)個位元組:

而使用「指向數組的指針」,每次前進的是這個數組的大小(即int[3]的大小,3*sizeof(int))
如圖所示,一次就前進了0x0c。

2.2.注意事項

指向數組的指針,類型表示:int (*a)[2];
而在表達式中,二維數組int a[2][2];的最高一維轉化為了指針,因此就變為了「指向數組的指針」。

另外,「指向數組的指針」和「指向數組首個元素的指針」是截然不同的。在表達式中,數組的最高維會被轉化為指針,此時的指針,指的是「指向數組初始元素的指針」。

int (*array_p)[3];

可以用來聲明一個指向「長度為3的數組」的指針。

在數組前,加上&取地址,返回的就是指向數組的指針:

int array[3];
int (*array_p)[3];

array_p=&array;

原本是int[3]類型,加上一層*,結果就是int (*)[3]
對於scanf在輸入字符串的時候,很多人還是這樣寫的:

scanf("%s",&s);

這樣寫看似沒有問題(實際上,由於後面的s在可變長參數中,沒有原型聲明,也不會出問題),但是是錯誤的寫法。因為s是數組,因此加上&後變為了「指向數組的指針」,而%s只需要傳入一個指向char的指針。
對於指向數組的指針,+1之後,真正加上的是它指向的數組的長度。(由於指針前進1,前進的是它所指向的值的大小)參考下圖:

2.3.數組與指針之間的轉化

  • 規則:在表達式中,數組的最高一維會被轉化為指針。
    • 特例1:對於sizeof(表達式)的形態,這是一個特例。(否則數組的長度是無法輸出的)
    • 特例2:在初始化char數組的時候,編譯器會自動把它解釋為初始化的列表。
char s[]="abc";

本質上是:

char s[]={'a','b','c','\0'};

的簡便寫法。這種解讀只有在初始化列表的時候可行。

  • 規則2:當數組解讀為指針時,這個指針不可作為左值。
    • 左值在英語中稱為”lvalue”,但是l並不代表left,而是locator(表示位置的事物)的意思。本質上,左值就是指可以出現在表達式的左邊,確切的說,就是有自己的內存空間,可以被賦值的東西、
    • 例如a=3中,a就是左值。而在a+1=3賦值語句中,由於a+1沒有自己的內存空間,無法被賦值,因此它不是左值。

例如:

char s[10];
s="abc";

這段代碼是錯誤的。

2.4.應用

在函數的參數中,二維數組會被解讀為指向數組的指針。

int f(int a[10][10]);

與下面等價:

int f(int a[][10]);
int f(int (*a)[10]);

但是注意,不能這麼寫:

int f(int a[][]);

因為指向數組的指針需要知道它的長度,不然在a++的時候,就不知道前進多少位置了。
當第二維的數字是2的時候,每次的前進是這樣的:

而當第二維的數字為5,每次的前進是這樣的:

而最高一維可以省略,因為無論最高維是多少,都和前進的位元組數無關。
對於數組a[m][n],二維數組的公式是a[i][j]=*(*(a+i)+j)。本質上說,二維數組可以看做「數組的數組」。
其中(a+i)每一次增加的長度也就是sizeof(a[i]),而sizeof(a[i])取決於它指向的內容(a可看做指向數組的指針,它的指向是j相關的一維),因此長度就是isizeof(int)n。
(a+i)+j中,加上的j前進的長度是sizeof(int)。

由此,我們可以看出,二維數組的尋址和j無關。

完。