C語言核心之陣列和指標詳解

wustrive發表於2016-12-04

指標

相信大家對下面的程式碼不陌生:

int i=2;                                            
int *p;                                             
p=&i;

這是最簡單的指標應用,也是最基本的用法。再來熟悉一下什麼是指標:首先指標是一個變數,它儲存的並不是平常的資料,而是變數的地址。如上程式碼,指標p中儲存的是整型變數i的地址資訊。

接下來看如何定義一個指標,既然指標也是一個變數,那麼它的定義也和其它變數一樣定義:如:int p;是間接定址或間接引用運算子。上例中我們還看到了一個特別的運算子&,它是一個取地址運算子(在其他合適場合&也是按位運算運算子,&&為取交集運算子)。

在上面的指標定義中,我們看到了定義的是一個整型指標,難道指標還有型別嗎?答案是肯定的,指標只能指向某種特定型別的物件,也就是說,每個指標都必須指向某種特定的資料型別(唯一的例外:指向void型別的指標可以存放指向任何型別的指標,但它不能間接引用其自身。)。比如,int 型別的指標絕對不能指向char 型別的變數。

下面我們給出一個完整的例子來說明指標的簡單應用:

#include<stdio.h>
void main()
{
    int a,b,c,*p;
    a=1;
    b=3;
    p=&a;
    b=*p+1;
    c=*(p+1);
    printf("%d %d %d %d /n",a,b,c,*p+3);
}

執行結果為: 1 2 -858993460 4

這是個完整的例子,可以自己在機器上除錯一下,現在很多人用的都是微軟的Visual Studio 開發環境,有人就不知道在該開發環境中怎麼寫C程式以及除錯C程式,具體境況可以參考附錄。

在上面例子中,看到了這樣兩個表示式b=p+1;和c=(p+1);前者的意思是p所指的地址裡的內容加1再賦給b,相當於b=a+1;,後者是p所指的地址加1再把(p+1)所指的地址賦給c,當然我們不知道p的下一個地址裡放的是什麼,所以輸出了一個隨機值(這樣的操作時很危險的,切記不要使用不確定的記憶體地址)。

陣列

陣列大家應該都很熟悉了,用途非常廣泛。

int a[4]={2,4,5,9};

此語句定義一個4個空間大小的整型陣列a併為它進行了初始化。

陣列的基礎知識可以參考其他相應的教材,我們在這主要討論指標和陣列的結合應用。

我們再來看個完整的例子:

#include<stdio.h>
void main()
{
    int a[4]={2,4,5,9};
    int *p;
    p=a;
    *p=*p++;
    printf("%d %d %d/n",*p,*p+6,*(p+1));
}

執行結果:4 10 5

分析:語句p=a;表示把陣列a的第0個元素的地址賦給指標p,陣列名a代表的是陣列a的第0個元素的地址。

a[i]表示陣列a的第i個元素,如果定義一個指標p,那麼語句p=&a[0];表示可以將指標p指向陣列a的第0個元素,也就是說p的值為陣列元素a[0]的地址。那麼(p+1)引用的是陣列元素a[1]的內容,p+i是陣列元素a[i]的地址,(p+i)引用的是陣列元素a[i]的內容。對陣列元素a[i]的引用也可以寫成(a+i)。可以得出結論:&a[i]與a+i的含義相同,p[i]與(p+i)也是等價的。

雖然陣列和指標有這麼多通用的地方,但我們必須記住,陣列名和指標之間有一個不同之處。指標是一個變數,因此語句p=a和p++都是合法的。但陣列名不是變數,因此,類似於a=p和a++形式的語句是非法的。

下面來看一個我們常用的函式strlen(char *s):

int strlen(char *s)
{
    int n;
    for(n=0;*s!='/0';s++)
          n++;
    return n;
}

因為s是一個指標,所以對其執行自增運算是合法的。執行s++運算不會影響到strlen函式的呼叫者中的字串,它僅對該指標在strlen函式中的私有副本進行自增運算。在函式定義中,形式引數char s[]和char *s是等價的。

我們再來看一下地址算術運算:如果p是一個指向陣列中某個元素的指標,那麼p++將對p進行自增運算並指向下一個元素,而p+=i將對p進行加i的增量運算,使其指向指標p當前所指向元素之後的第i個元素。同其他型別的變數一樣,指標也可以進行初始化。通常,對指標有意義的初始化值只能是0或者是表示地址的表示式,對後者來說,表示式所表達的地址必須是在此之前已定義的具有適當型別的資料的地址。任何指標與0進行相等或者不相等的比較運算都有意義。但是指向不同陣列的元素的指標之間的算術或比較運算沒有意義。指標還可以和整數進行相加或相減運算。如p+n表示指標p當前指向的物件之後第n個物件的地址。無論指標p指向的物件是何種型別,上述結論都成立。在計算p+n時,n將根據p指向的物件的長度按比例縮放,而p指向的物件的長度則取決於p的宣告。例如,如果int型別佔4個位元組的儲存空間,那麼在int型別的計算中對應的n將按4的倍數來計算。

指標的減法運算也是有意義的,如果p和q指向相同陣列中的元素,且p<q,那麼q-p+1就是位於p和q指向的元素之間的元素的數目。我們來看一下strlen(char *s)的另一個版本:

int strlen(char *s)
{
   char *p=s;
   while(*p!='/0')
          p++;
   return p-s;
}

程式中,p被初始化為指向s,即指向該字串的第一個字元,while迴圈語句將依次檢查字串中的每個字元,直到遇到標識字元陣列結尾的字元’/0’為止。由於p是指向字元的指標,所以每執行以此p++,p就將指向下一個字元的地址,p-s則表示已經檢查過的字元數,即字串長度。

總結:有效的指標運算包括相同型別指標之間的賦值運算;指標和整數之間的加減運算;指向相同陣列中元素的兩個指標間的減法或比較運算;將指標賦值為0或指標與0之間的比較運算。其他所有形式的指標運算都是非法的。

再來看兩條語句:

char  a[]=”I  am  a  boy”;  char *p=”I  am  a  boy”;

a是一個僅僅足以存放初始化字串以及空字元’/0’的一維陣列。陣列中的單個字元可以進行修改,但a始終指向同一個儲存位置。而p是一個指標,其初值指向一個字串常量,之後它可以被修改以指向其他地址,但如果試圖修改字串的內容,結果是沒有定義的。

為了更容易理解陣列和指標的關係,我們再來看一個函式:

void strcpy(char *s,char *t)
{
   int i;
   i=0;
   while((s[i]=t[i])!='/0')
          i++;
}

因為引數是通過值傳遞的,所以在strcpy函式中可以以任何方式使用引數s和t。

下面是指標實現的幾個版本:

void strcpy(char *s,char *t)
{
   while((*s=*t)!='/0'){
          s++;
          t++;
   }
}

最簡版本:

void strcpy(char *s,char *t)
{
   while(*s++=*t++)
          ;
}

這裡,s和t的自增運算放到了迴圈的測試部分中。表示式*t++的值是執行自增運算之前t所指向的字元。字尾運算子++表示在讀取該字元之後才改變t的值。同樣,在s執行自增運算之前,字元就被儲存到了指標s指向的舊位置。上面的版本中表示式同’/0’的比較是多餘的,因為只需要判斷表示式的值是否為0即可。

指標陣列和指向指標的指標

這兩個詞次聽起來挺新穎的,到底是什麼意思呢?

由於指標本身也是變數,所以它們也可以像其他變數一樣儲存在陣列中。這一點很容易理解。

#include<stdio.h>
#include<string.h>
void main()
{
   int i;
   char b[]={"wustrive_2008"};
   char *a[1];
   *a=b;
   for(i=0;i<strlen(b);i++)
          printf("%c",*(a[0]+i));
   printf("/n");
}

執行結果:wustrive_2008

這裡庫函式strlen,strlen為string類的標準庫函式,所以要包含#include。

下面我們來自己寫一個strlen函式,我們把上面的例子該成這樣:

#include<stdio.h>
int strlen(char *s)
{
   char *p=s;
   while(*p!='/0')
          p++;
   return p-s;
}
void main()
{
   int i;
   char b[]={"wustrive_2008"};
   char *a[1];
   *a=b;
   for(i=0;i<strlen(b);i++)
          printf("%c",*(a[0]+i));
   printf("/n");
}

這個執行結果和上個例子一樣,不一樣的只是我們自己實現了strlen函式,我們再程式設計時使用的庫函式,都是語言的開發者或者系統為我們寫好了的函式,其實我們也可以自己寫。

這個例子很好的演示了指標陣列的用法,指標陣列a的值a[1]是一個指標,指向字元陣列第一個字元。

指標的指標也很好理解,就是一個指標裡放的是另一個指標的地址,而另一個指標可能指向一個變數的地址,還可能指向另一個指標。

指標和多維陣列

看兩個定義語句:int a[5][10]; int *b[5];

從語法角度講,a[3][4]和b[3][4]都是對一個int物件的合法引用。但a是一個真正的二維陣列,它分配了50個int型別長度的儲存空間。但b定義僅僅分配了5個指標,並且沒有初始化,它們必須進行顯示的初始化,假設b的每個元素都指向一個有10個元素的陣列,那麼編譯器就要為它分配50個int型別長度的儲存空間以及5個指標儲存空間。指標陣列的一個重要優點在於,陣列的每一行長度可以不同,也就是說,b的每個元素不必都指向一個有10個元素的向量。

指向函式的指標:

在C語言中,函式雖然不是變數,但可以定義指向函式的指標。這種型別的指標可以被賦值,存放在陣列中,傳遞給函式以及作為函式的返回值等。

如果下面的語句為一個函式的引數,表示什麼意思:

int (p)(void ,void *)

它表明p是一個指向函式的指標,該函式具有兩個void型別的引數,其返回值型別為int。語句if((p)(v[i],v[left])<0)中,p的使用和其宣告是一致的,p是一個指向函式的指標,p代表一個函式。如果寫成這樣:int p(void ,void )則表明p是一個函式,該函式返回一個int型別的指標。

下面來看兩個宣告:

int  *f();      //f是一個函式,它返回一個指向int型別的指標
int   (*pf)();     //pf是一個指向函式的指標,該函式返回一個int型別的物件。

相關文章