1. 程式人生 > 其它 >C語言--指標詳解(轉)

C語言--指標詳解(轉)

 這段時間在看 Linux 核心,深覺 C 語言功底不紮實,很多程式碼都看不太懂,深入學習鞏固 C 語言的知識很有必要。先從指標開始。

一、什麼是指標

  C語言裡,變數存放在記憶體中,而記憶體其實就是一組有序位元組組成的陣列,每個位元組有唯一的記憶體地址。CPU 通過記憶體定址對儲存在記憶體中的某個指定資料物件的地址進行定位。這裡,資料物件是指儲存在記憶體中的一個指定資料型別的數值或字串,它們都有一個自己的地址,而指標便是儲存這個地址的變數。也就是說:指標是一種儲存變數地址的變數

  前面已經提到記憶體其實就是一組有序位元組組成的陣列,陣列中,每個位元組大大小固定,都是 8bit。對這些連續的位元組從 0 開始進行編號,每個位元組都有唯一的一個編號,這個編號就是記憶體地址。示意如下圖:

  這是一個 4GB 的記憶體,可以存放 2^32 個位元組的資料。左側的連續的十六進位制編號就是記憶體地址,每個記憶體地址對應一個位元組的記憶體空間。而指標變數儲存的就是這個編號,也即記憶體地址。

二、為什麼要使用指標

  在C語言中,指標的使用非常廣泛,因為使用指標往往可以生成更高效、更緊湊的程式碼。總的來說,使用指標有如下好處:

1)指標的使用使得不同區域的程式碼可以輕易的共享記憶體資料,這樣可以使程式更為快速高效;

2)C語言中一些複雜的資料結構往往需要使用指標來構建,如連結串列、二叉樹等;

3)C語言是傳值呼叫,而有些操作傳值呼叫是無法完成的,如通過被調函式修改呼叫函式的物件,但是這種操作可以由指標來完成,而且並不違背傳值呼叫。

三、如何宣告一個指標

3.1 宣告並初始化一個指標

  指標其實就是一個變數,指標的宣告方式與一般的變數宣告方式沒太大區別:

int *p;        // 宣告一個 int 型別的指標 p
char *p        // 宣告一個 char 型別的指標 p
int *arr[10]   // 宣告一個指標陣列,該陣列有10個元素,其中每個元素都是一個指向 int 型別物件的指標
int (*arr)[10] // 宣告一個數組指標,該指標指向一個 int 型別的一維陣列
int **p;       // 宣告一個指標 p ,該指標指向一個 int 型別的指標

  指標的宣告比普通變數的宣告多了一個一元運算子 “*”。運算子 “*” 是間接定址或者間接引用運算子。當它作用於指標時,將訪問指標所指向的物件。在上述的宣告中: p 是一個指標,儲存著一個地址,該地址指向記憶體中的一個變數; *p 則會訪問這個地址所指向的變數。

  宣告一個指標變數並不會自動分配任何記憶體。在對指標進行間接訪問之前,指標必須進行初始化:或是使他指向現有的記憶體,或者給他動態分配記憶體,否則我們並不知道指標指向哪兒,這將是一個很嚴重的問題,稍後會討論這個問題。初始化操作如下:

/* 方法1:使指標指向現有的記憶體 */
int x = 1;
int *p = &x;  // 指標 p 被初始化,指向變數 x ,其中取地址符 & 用於產生運算元記憶體地址

/* 方法2:動態分配記憶體給指標 */
int *p;
p = (int *)malloc(sizeof(int) * 10);    // malloc 函式用於動態分配記憶體
free(p);    // free 函式用於釋放一塊已經分配的記憶體,常與 malloc 函式一起使用,要使用這兩個函式需要標頭檔案 stdlib.h

  指標的初始化實際上就是給指標一個合法的地址,讓程式能夠清楚地知道指標指向哪兒。

3.2 未初始化和非法的指標

   如果一個指標沒有被初始化,那麼程式就不知道它指向哪裡。它可能指向一個非法地址,這時,程式會報錯,在 Linux 上,錯誤型別是 Segmentation fault(core dumped),提醒我們段違例或記憶體錯誤。它也可能指向一個合法地址,實際上,這種情況更嚴重,你的程式或許能正常執行,但是這個沒有被初始化的指標所指向的那個位置的值將會被修改,而你並無意去修改它。用一個例子簡單的演示一下:

#include "stdio.h"

int main(){
    int *p;
    *p = 1;
    printf("%d\n",*p);

    return 0;  
}

  這個程式可以編譯通過,但是執行的話會報錯,報錯資訊如下:

  要想使這個程式執行起來,需要先對指標 p 進行初始化:

#include "stdio.h"

int main(){
    int x = 1;  
    int *p = &x;
    printf("%d\n",*p);
   *p = 2;
    printf("%d\n",*p);

    return 0;  
}

  這段程式碼的輸出結果如下:

  可以看到,對指標進行初始化後,便可以正常對指標進行賦值了。

3.3 NULL指標

  NULL 指標是一個特殊的指標變數,表示不指向任何東西。可以通過給一個指標賦一個零值來生成一個 NULL 指標。

#include "stdio.h"

int main(){
    int *p = NULL;
    printf("p的地址為%p\n",p);

    return 0;
}

/***************
 * 程式輸出:
 * p的地址為(nil)
***************/

  可以看到指標的地址為nil,即空值,也可以理解為0。在大多數的作業系統上,程式不允許訪問地址為 0 的記憶體,因為該記憶體是為作業系統保留的。但是,記憶體地址 0 有一個特別重要的意義,它表明改指標不指向一個可訪問的記憶體位置。

四、指標的運算

  C 指標的算術運算只限於兩種形式:

1) 指標 +/- 整數 :

   可以對指標變數 p 進行 p++、p--、p + i 等操作,所得結果也是一個指標,只是指標所指向的記憶體地址相比於 p 所指的記憶體地址前進或者後退了 i 個運算元。用一張圖來說明一下:

  在上圖中,10000000等是記憶體地址的十六進位制表示(數值是假定的),p 是一個 int 型別的指標,指向記憶體地址 0x10000008 處。則 p++ 將指向與 p 相鄰的下一個記憶體地址,由於 int 型資料佔 4 個位元組,因此 p++ 所指的記憶體地址為 1000000c。其餘類推。不過要注意的是,這種運算並不會改變指標變數 p 自身的地址,只是改變了它所指向的地址。舉個例子:

2)指標 - 指標

  只有當兩個指標都指向同一個陣列中的元素時,才允許從一個指標減去另一個指標。兩個指標相減的結果的型別是 ptrdiff_t,它是一種有符號整數型別。減法運算的值是兩個指標在記憶體中的距離(以陣列元素的長度為單位,而不是以位元組為單位),因為減法運算的結果將除以陣列元素型別的長度。舉個例子:

#include "stdio.h"

int main(){
    int a[10] = {1,2,3,4,5,6,7,8,9,0};
    int sub;
    int *p1 = &a[2];
    int *p2 = &a[8];

    sub = p2-p1;                                                                            
    printf("%d\n",sub);    // 輸出結果為 6

    return 0;
}

五、指標與陣列

  在C語言中,指標與陣列之間的關係十分密切。實際上,許多可以用陣列完成的工作都可以使用指標來完成。一般來說,用指標編寫的程式比用陣列編寫的程式執行速度快,但另一方面,用指標實現的程式理解起來稍微困難一些。

5.1 指標與陣列的關係

  我們先宣告一個數組:

int a[10];        // 宣告一個int型別的陣列,這個陣列有10個元素

  我們可以用 a[0]、a[1]、...、a[9] 來表示這個陣列中的10個元素,這10個元素是儲存在一段連續相鄰的記憶體區域中的。

  接下來,我們再宣告一個指標:

int *p;           // 宣告一個int型別的指標變數

   p 是一個指標變數,指向記憶體中的一個區域。如果我們對指標 p 做如下的初始化:

p = &a[0];        // 對指標進行初始化,p將指向陣列 a 的第 1 個元素 a[0]

  我們知道,對指標進行自增操作會讓指標指向與當前元素相鄰的下一個元素,即 *(p + 1) 將指向 a[1] ;同樣的, *(p + i) 將指向 a[i] 。因此,我們可以使用該指標來遍歷陣列 a[10] 的所有元素。可以看到,陣列下標與指標運算之間的關係是一一對應的。而根據定義,陣列型別的變數或表示式的值是該陣列第 1 個元素的地址,且陣列名所代表的的就是該陣列第 1 個元素的地址,故,上述賦值語句可以直接寫成:

p = a;        // a 為陣列名,代表該陣列最開始的一個元素的地址

  很顯然,一個通過陣列和下標實現的表示式可以等價地通過指標及其偏移量來實現,這就是陣列和指標的互通之處。但有一點要明確的是,陣列和指標並不是完全等價,指標是一個變數,而陣列名不是變數,它陣列中第 1 個元素的地址,陣列可以看做是一個用於儲存變數的容器。更直接的方法,我們可以直接看二者的地址,並不一樣:

#include "stdio.h"                                                                          
 
int main(){
    int x[10] = {1,2,3,4,5,6,7,8,9,0};
    int *p = x;
    printf("x的地址為:%p\n",x);
    printf("x[0]的地址為:%p\n",&x[0]);
    printf("p的地址為:%p\n",&p);      // 列印指標 p 的地址,並不是指標所指向的地方的地址

    p += 2;
    printf("*(p+2)的值為:%d\n",*p);    // 輸出結果為 3,*(p+2)指向了 x[2]

    return 0;
}

  結果如下:

  可以看到,x 的值與 x[0] 的地址是一樣的,也就是說陣列名即為陣列中第 1 個元素的地址。實際上,列印 &x 後發現,x 的地址也是這個值。而 x 的地址與指標變數 p 的地址是不一樣的。故而陣列和指標並不能完全等價。

(筆者注:上述輸出結果是在 centos7 64bit 的環境下使用 gcc 編譯器得到的,可以看到地址是一個12位的十六進位制數,轉換成二進位制是48位,也就是說定址空間有 256TB,但是筆者的電腦只有 8GB 記憶體,猜測是不是由於 linux 系統開啟了記憶體分頁機制,這裡定址的是虛擬地址?另外,在Windows下使用 vs2015 編譯執行的話,則輸出結果是一個 8位的十六進位制數,也就是32位二進位制,定址空間為 4GB)

5.2 指標陣列

  指標是一個變數,而陣列是用於儲存變數的容器,因此,指標也可以像其他變數一樣儲存在陣列中,也就是指標陣列。指標陣列是一個數組,陣列中的每一個元素都是指標。宣告一個指標陣列的方法如下:

int *p[10];    // 宣告一個指標陣列,該陣列有10個元素,其中每個元素都是一個指向int型別的指標

  在上述宣告中,由於 [] 的優先順序比 * 高,故 p 先與 [] 結合,成為一個數組 p[];再由 int * 指明這是一個 int 型別的指標陣列,陣列中的元素都是 int 型別的指標。陣列的第 i 個元素是 *p[i],而p[i] 是一個指標。由於指標陣列中存放著多個指標,操作靈活,在一些需要操作大量資料的程式中使用,可以使程式更靈活快速。

5.3 陣列指標

  陣列指標是一個指標,它指向一個數組。宣告一個數組指標的方法如下:

int (*p)[10];        // 宣告一個數組指標 p ,該指標指向一個數組

  由於 () 的優先順序最高,所以 p 是一個指標,指向一個 int 型別的一維陣列,這個一維陣列的長度是 10,這也是指標 p 的步長。也就是說,執行 p+1 時,p 要跨過 n 個 int 型資料的長度。陣列指標與二維陣列聯絡密切,可以用陣列指標來指向一個二維陣列,如下:

 #include "stdio.h"
 
 int main(){
     int arr[2][3] = {1,2,3,4,5,6};               // 定義一個二維陣列並初始化
     int (*p)[3];                                 // 定義一個數組指標,指標指向一個含有3個元素的一維陣列
 
     p = arr;                                     // 將二維陣列的首地址賦給 p,此時 p 指向 arr[0] 或 &arr[0][0]
     printf("%d\n",(*p)[0]);              // 輸出結果為 1
     p++;                         // 對 p 進行算術運算,此時 p 將指向二維陣列的下一行的首地址,即 &arr[1][0]
     printf("%d\n",(*p)[1]);                      // 輸出結果為5
 
     return 0;                                                                               
 }

六、指標與結構

6.1 簡單介紹一下結構

  結構是一個或多個變數的集合,這些變數可能為不同的型別,為了處理的方便而將這些變數組織在一個名字之下。由於結構將一組相關的變數看做一個單元而不是各自獨立的實體,因此結構有助於組織複雜的資料,特別是在大型的程式中。宣告一個結構的方式如下:

struct message{            // 宣告一個結構 message
    char name[10];             // 成員
    int age;
    int score;  
};

typedef struct message s_message;     // 型別定義符 typedef

s_message mess = {"tongye",23,83};    // 宣告一個 struct message 型別的變數 mess,並對其進行初始化 

-------------------------------------------------------------------------------------------------------------- /* 另一種更簡便的宣告方法 */ typedef struct{   char name[10];   int age;   int score; }message;

  可以使用結構名.成員的方式來訪問結構中的成員,如下:

#include "stdio.h"

int main(){
  printf("%s\n",mess.name);    // 輸出結果:tongye
  printf("%d\n",mess.age);     // 輸出結果:23

  return 0;
}

6.2 結構指標 

  結構指標是指向結構的指標,以上面的結構為例,可以這樣定義一個結構指標:

s_message *p;        // 宣告一個結構指標 p ,該指標指向一個 s_message 型別的結構
*p = &mess;      // 對結構指標的初始化與普通指標一樣,也是使用取地址符 &

  C語言中使用 -> 操作符來訪問結構指標的成員,舉個例子:

#include "stdio.h"

typedef struct{
    char name[10];
    int age;
    int score;  
}message;

int main(){
    message mess = {"tongye",23,83};
    message *p = &mess;

    printf("%s\n",p->name);      // 輸出結果為:tongye
    printf("%d\n",p->score);         // 輸出結果為:83

    return 0;
}

七、指標與函式

  C語言的所有引數均是以“傳值呼叫”的方式進行傳遞的,這意味著函式將獲得引數值的一份拷貝。這樣,函式可以放心修改這個拷貝值,而不必擔心會修改呼叫程式實際傳遞給它的引數。

7.1 指標作為函式的引數

  傳值呼叫的好處是是被調函式不會改變呼叫函式傳過來的值,可以放心修改。但是有時候需要被調函式回傳一個值給呼叫函式,這樣的話,傳值呼叫就無法做到。為了解決這個問題,可以使用傳指標呼叫。指標引數使得被調函式能夠訪問和修改主調函式中物件的值。用一個例子來說明:

#include "stdio.h"

void swap1(int a,int b)        // 引數為普通的 int 變數
{
  int temp;
  temp = a;
  a = b;
  b = temp;
}

void swap2(int *a,int *b)      // 引數為指標,接受呼叫函式傳遞過來的變數地址作為引數,對所指地址處的內容進行操作
{
  int temp;                    // 最終結果是,地址本身並沒有改變,但是這一地址所對應的記憶體段中的內容發生了變化,即x,y的值發生了變化
  temp = *a;
  *a = *b;
  *b = temp;
}

int main()
{
  int x = 1,y = 2;
  swap1(x,y);                     // 將 x,y 的值本身作為引數傳遞給了被調函式
  printf("%d %5d\n",x,y);         // 輸出結果為:1     2

  swap(&x,&y);                    // 將 x,y 的地址作為引數傳遞給了被調函式,傳遞過去的也是一個值,與傳值呼叫不衝突
  printf("%d %5d\n",x,y);         // 輸出結果為:2     1
  return 0;
}

7.2 指向函式的指標

  在C語言中,函式本身不是變數,但是可以定義指向函式的指標,也稱作函式指標,函式指標指向函式的入口地址。這種型別的指標可以被賦值、存放在陣列中、傳遞給函式以及作為函式的返回值等等。宣告一個函式指標的方法如下:

返回值型別 (* 指標變數名)([形參列表]);

int (*pointer)(int *,int *);        // 宣告一個函式指標

  上述程式碼聲明瞭一個函式指標 pointer ,該指標指向一個函式,函式具有兩個 int * 型別的引數,且返回值型別為 int。下面的程式碼演示了函式指標的用法:

#include "stdio.h"
#include "string.h"

int str_comp(const char *m,const char *n);                             // 宣告一個函式 str_comp,該函式有兩個 const char 型別的指標,函式的返回值為 int 型別
void comp(char *a,char *b,int (*prr)(const char *,const char*));       // 宣告一個函式 comp ,注意該函式的第三個引數,是一個函式指標

int main()
{
    char str1[20];      // 宣告一個字元陣列
    char str2[20];
    int (*p)(const char *,const char *) = str_comp;            // 宣告並初始化一個函式指標,該指標所指向的函式有兩個 const char 型別的指標,且返回值為 int 型別
    gets(str1);         // 使用 gets() 函式從 I/O 讀取一行字串
    gets(str2);
    comp(str1,str2,p);  // 函式指標 p 作為引數傳給 comp 函式

    return 0;
}

int str_comp(const char *m,const char *n)
{
   // 庫函式 strcmp 用於比較兩個字串,其原型是: int strcmp(const char *s1,const char *s2);
    if(strcmp(m,n) == 0) 
        return 0;
    else
        return 1;
}

/* 函式 comp 接受一個函式指標作為它的第三個引數 */
void comp(char *a,char *b,int (*prr)(const char *,const char*))
{
    if((*prr)(a,b) == 0)
        printf("str1 = str2\n");
    else
        printf("str1 != str2\n");
} 

  這段程式碼的功能是從鍵盤讀取兩行字串(長度不超過20),判斷二者是否相等。

  注意,宣告一個函式指標時,() 不能漏掉,否則:

int *p(void *,void*);

  這表明 p 是一個函式,該函式返回一個指向 int 型別的指標。

參考文獻:

1)C程式設計語言(第2版)

2)C和指標

參考:

https://blog.csdn.net/soonfly/article/details/51131141

https://www.cnblogs.com/lulipro/p/7460206.html

--------------------------------------------------------------------------------------------------------