【原創】淺談指標(一)

計算機知識雜談發表於2021-10-03

前言

如今的很多開發人員,對指標或多或少都有一些畏懼心理,都認為“指標經常會在一些不起眼的地方讓程式崩潰”。確實,很多錯誤都是由於指標引起的。指標和記憶體密切相關,難免會由於地址或是陣列越界,沒有初始化等原因,導致程式崩潰,然而,其實大多數錯誤都是可以避免的。
其實本人也看過一本書,叫做《征服C指標》。這本書寫的非常好,也希望大家有機會可以看看。這本書其實更多是針對C語言的語法和記憶體內部原理介紹。而本文將會更多的說到它的實際應用。

為什麼需要指標

在不知不覺中,我們其實已經在使用指標了。很久以前,當那些老師教我們使用scanf的時候,一定會說:scanf引數中變數名前要加上“&”號。

scanf("%d %d",a,b);

其實,這裡&符號的應用,暗含著指標和引數傳遞的操作。
其實scanf是非常老的函式了,在最早的C語言就有出現。那時候還沒有C++的引用機制,由於函式傳值機制,因此必須使用指標。
&符號是取地址運算子,我們可以嘗試定義變數並輸出他的地址。

#include<bits/stdc++.h>
using namespace std;
int qj;
int main(){
    int jb;
    cout<<&qj<<' '<<&jb;
}

這個程式嘗試輸出全域性變數和區域性變數的地址。可以看到,地址一般由十六進位制的格式輸出。

記憶體地址

講了這麼多,我們還沒有介紹我們的記憶體。記憶體是一個儲存器,一般家用記憶體從2GB到32GB不等。通常,我們執行的程式都儲存在記憶體之中。有人會問,我們寫程式都是儲存在C/D盤中的,不是在磁碟裡面嗎?其實,磁碟的讀取速度相較記憶體是非常緩慢的。因此,這類工作都是把磁碟中的資料先複製到記憶體,再執行其中的程式碼。
記憶體很大,為了把記憶體每一個位元組做上標記,我們需要一個數字作為記憶體的標號,這個標號就是地址。可以理解成我們生活中的身份證號一樣,根據這個號碼,我們就可以定位到一個唯一的位置了。(具體說,一般大多數計算機都是把程式放進虛擬記憶體的,本文此處不展開詳細描述,請參見其他資料)

指標變數

指標變數可以理解成儲存一個地址的變數。一般的變數定義如下:型別 *變數名
例如int *a;
乍一看像是定義叫做a的變數,但是其實定義一個叫做a的變數,型別是int

a這個變數用於儲存變數的地址,&運算子也用於取得變數地址,那麼,我們就可以這樣使用指標了:

#include<bits/stdc++.h>
using namespace std;
int a=10;
int *p;
int main(){
    p=&a;
    printf("p..%p\n",p);
    printf("a..%d\n",a);
    printf("*p..%d",*p);
}

*p用於取得p地址的數值,到目前為止,我們學習了兩個運算子:
&a 取得變數a的地址
*a 取得a號地址的數值

指標的加減運算

#include<bits/stdc++.h>
using namespace std;
int a[3]={1,2,3};
int *p;
int main(){
    cout<<a<<endl;
    p=a;
    ++p;
    cout<<p<<endl;
    cout<<*p<<endl;
}

輸出:(前兩個地址可能不同,但是相差一定為4)
0x601508
0x60150c
2

可以看到,其實陣列名a就是一個指向陣列首元素的指標,不同的是,a是一個常量,不能對a進行修改(例如a++)
因此,a是陣列首元素的地址,自然可以賦值為p。
p++後,我們看到,結果不是0x601509,因為+1後,實際p不是加了1而是增加了sizeof(int),即讓p到達下一個陣列元素的地址。

我們發現:
p+1後,*p就等於*(p+1)
而根據指標運演算法則,*(p+1)=a[1]
更加進一步說,a就是p,那麼
*(p+1)=p[1]
推廣開來,其實就是這個著名的指標定理:
p[i]=*(p+i)

進一步說,我們發現:
p[i]
=*(p+i) (指標的性質)
=*(i+p) (加法交換律)
=i[p] (指標的性質)

所以p[i]=i[p]。

指標運算的作用

使用指標訪問陣列元素

#include<bits/stdc++.h>
using namespace std;
int a[10]={4,6,3,7,8,4,5,9,1,2};
int main(){
    for(int *p=a;p<a+10;p++){
        cout<<*p<<endl;
    }
}

我們利用了指標進行了訪問陣列元素。
其實更好,更簡便的寫法是直接使用下標訪問a[i]。很多書上用這種寫法說明指標的優越性:

但是,a[i]=*(a+i),如果迴圈中多次出現,那麼a+i就要被計算多次。但是用指標的方法,迴圈結束時才計算一次p++,因此速度較快。

然而這段話已經過時了,現代的編譯器大多有自動優化功能,會自動優化這些a+i的計算,對編譯器而言,寫成指標和陣列並無什麼不同。因此,寫這段的目的是:在非必要情況下,少用指標。指標用在這種場合就是殺雞用牛刀了。

下標為負數的陣列

#include<bits/stdc++.h>
using namespace std;
int a[10]={4,6,3,7,8,4,5,9,1,2};
int main(){
    int *p=&a[5];
    cout<<p[-1]<<endl;
}

p指向的是第5個元素,那麼p-1就是指向第4個元素8。把指標指向陣列中間元素,然後就可以把下標置換為負數了。

可變長陣列(也稱動態陣列)

如果我們要定義一個陣列,但是長度要等執行時確定,我們可以使用指標進行分配。

#include<bits/stdc++.h>
using namespace std;
int *p,n;
int main(){
    cin>>n;
    p=(int*)malloc(n*sizeof(int));
    for(int i=0;i<n;i++)cin>>p[i];
    for(int i=0;i<n;i++)cout<<p[i]<<" ";
}

使用malloc函式,給p分配n個空間,由於空間是連續的,因此我們可以把p當作陣列使用。

時間關係,更多內容敬請期待下一篇文章:淺談指標(二)
大致內容:

  • 函式傳參機制
  • 引用和指標的異同
  • 應用:連結串列
  • 函式指標

相關文章