【原創】淺談指針(一)

前言

如今的很多開發人員,對指針或多或少都有一些畏懼心理,都認為「指針經常會在一些不起眼的地方讓程序崩潰」。確實,很多錯誤都是由於指針引起的。指針和內存密切相關,難免會由於地址或是數組越界,沒有初始化等原因,導致程序崩潰,然而,其實大多數錯誤都是可以避免的。
其實本人也看過一本書,叫做《征服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當作數組使用。

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

  • 函數傳參機制
  • 引用和指針的異同
  • 應用:鏈表
  • 函數指針
Tags: