深入理解C語言 - 指針詳解


一、什么是指針

C語言里,變量存放在內存中,而內存其實就是一組有序字節組成的數組,每個字節有唯一的內存地址。CPU 通過內存尋址對存儲在內存中的某個指定數據對象的地址進行定位。這里,數據對象是指存儲在內存中的一個指定數據類型的數值或字符串,它們都有一個自己的地址,而指針便是保存這個地址的變量。也就是說:指針是一種保存變量地址的變量。

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

這是一個 4GB 的內存,可以存放 2^32 個字節的數據。左側的連續的十六進制編號就是內存地址,每個內存地址對應一個字節的內存空間。而指針變量保存的就是這個編號,也即內存地址。


二、為什么要使用指針

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

1)指針的使用使得不同區域的代碼可以輕易的共享內存數據,這樣可以使程序更為快速高效;

2)C語言中一些復雜的數據結構往往需要使用指針來構建,如鏈表、二叉樹等;

3)C語言是傳值調用,而有些操作傳值調用是無法完成的,如通過被調函數修改調用函數的對象,但是這種操作可以由指針來完成,而且並不違背傳值調用。


三、如何聲明一個指針

指針其實就是一個變量,指針的聲明方式與一般的變量聲明方式沒太大區別:

int *p; //聲明一個返回整型數據的指針 

int *p[3]; //因為[]的優先級比*高,所以P是一個數組,因此P是一個由返回整型數據的指針所組成的數組

int (*p)[3]; //首先P是一個指針,然后再與[]結合,說明指針所指向的內容是一個數組,所以P是一個指向由整型數據組成的數組的指針

int (*p)(int); //首先P是一個指針,然后與()結合,說明指針指向的是一個函數,所以P 是一個指向有一個整型參數且返回類型為整型的函數的指針  

int *(*p(int))[3]; //P是一個參數為一個整數且返回一個指向由整型指針變量組成的數組的指針變量的函數

指針的聲明比普通變量的聲明多了一個一元運算符 “*”。運算符 “*” 是間接尋址或者間接引用運算符。當它作用於指針時,將訪問指針所指向的對象。在上述的聲明中: 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
p = NULL;

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


四、細說指針

指針是一個特殊的變量,它里面存儲的數值被解釋成為內存里的一個地址。要搞清一個指針需要搞清指針的四方面的內容:指針的類型、指針所指向的類型、指針的值或者叫指針所指向的內存區、指針本身所占據的內存大小。

先聲明幾個指針作為例子:

int*ptr;  
char*ptr;  
int**ptr;  
int(*ptr)[3];

1.指針的類型

從語法的角度看,你只要把指針聲明語句里的指針名字去掉,剩下的部分就是這個指針本身的類型。讓我們看看上面例子中各個指針的類型:

int*ptr; //指針的類型是int* 
char*ptr; //指針的類型是char* 
int**ptr; //指針的類型是int**
int(*ptr)[3]; //指針的類型是int(*)[3]

2.指針所指向的類型

當你通過指針來訪問指針所指向的內存區時,指針所指向的類型決定了編譯器將把那片內存區里的內容當做什么來看待。

從語法上看,你只須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉,剩下的就是指針所指向的類型。例如:

int*ptr; //指針所指向的類型是int
char*ptr; //指針所指向的的類型是char
int**ptr; //指針所指向的的類型是int*
int(*ptr)[3]; //指針所指向的的類型是int()[3]

3.指針的值或者叫指針所指向的內存區

指針的值是指針本身存儲的數值,這個值將被編譯器當作一個地址,而不是一個一般的數值。在32 位程序里,所有類型的指針的值都是一個32 位整數,因為32 位程序里內存地址全都是32 位長。

指針所指向的內存區就是從指針的值所代表的那個內存地址開始,長度為sizeof(指針所指向的類型)的一片內存區。以后,我們說一個指針的值是XX,就相當於說該指針指向了以XX 為首地址的一片內存區域;我們說一個指針指向了某塊內存區域,就相當於說該指針的值是這塊內存區域的首地址。


4.指針本身所占據的內存大小

指針本身占了多大的內存?你只要用函數sizeof(指針的類型)測一下就知道了。在32 位平台里,指針本身占據了4 個字節的長度。


五、指針的算術運算

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

1) 指針 +/- 整數 :

可以對指針變量 p 進行 p++、p--、p + i 等操作,所得結果也是一個指針,只是指針所指向的內存地址相比於 p 所指的內存地址前進或者后退了 i 個操作數。用一張圖來說明一下:

在上圖中,10000000等是內存地址的十六進制表示(數值是假定的),p 是一個 int 類型的指針,指向內存地址 0x10000008 處。則 p++ 將指向與 p 相鄰的下一個內存地址,由於 int 型數據占 4 個字節,因此 p++ 所指的內存地址為 1000000b。其余類推。不過要注意的是,這種運算並不會改變指針變量 p 自身的地址,只是改變了它所指向的地址。舉個例子:

#include "stdio.h"

int main()
{
    char arr[20]="the_example";
    int *ptr=(int *)arr;
    ptr++;
    printf("%c",*ptr); //輸出: arr[4] - 'e'

    return 0;
}

在上例中,指針ptr 的類型是int*,它指向的類型是int,它被初始化為指向數組arr。接下來指針ptr被加了1,編譯器是這樣處理的:它把指針ptr 的值加上了sizeof(int),在32位程序中,是被加上了4。由於地址是用字節做單位的,故ptr 所指向的地址由原來的數組arr的地址向高地址方向增加了4個字節。由於char 類型的長度是一個字節,所以,原來ptr是指向數組arr的第0個字節開始的四個字節,此時指向了數組a中從第4個字節開始的四個字節。


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;
}

六、指針與數組的關系

數組的數組名其實可以看作一個指針,一個通過數組和下標實現的表達式可以等價地通過指針及其偏移量來實現,這就是數組和指針的互通之處。但有一點要明確的是,數組和指針並不是完全等價,指針是一個變量,而數組名不是變量,它數組中第 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 的地址是不一樣的。故而數組和指針並不能完全等價。


七、指針與結構的關系

結構指針是指向結構的指針,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->mess);      // 輸出結果為:tongye
    printf("%d\n",p->score);         // 輸出結果為:83

    return 0;
}

八、指針和函數的關系

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


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;
}

2.指向函數的指針

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

返回值類型 (* 指針變量名)([形參列表]);

int (*pointer)(int *,int *);        // 聲明一個函數指針

上述代碼聲明了一個函數指針 pointer ,該指針指向一個函數,函數具有兩個 int * 類型的參數,且返回值類型為 int。下面的代碼演示了函數指針的用法:

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

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");
}       // 聲明一個函數 comp ,注意該函數的第三個參數,是一個函數指針

int main()
{
    char str1[20]; //聲明一個字符數組
    char str2[20];
    int (*p)(const char *,const char *) = str_comp; //聲明並初始化一個函數指針

    gets(str1); //使用 gets() 函數從 I/O 讀取一行字符串
    gets(str2);
    comp(str1,str2,p); //函數指針 p 作為參數傳給 comp 函數

    return 0;
}

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


2.使用typedef定義函數指針

示例如下:

void MyFun(int x)
{
   printf("%d\n",x);
}

typedef void (*funP)(int); //定義函數指針類型

int main(int argc, char* argv[])
{
    funP p;
    p=&MyFun; //將MyFun函數的地址賦給FunP變量
    (*p)(20); //這是通過函數指針變量FunP來調用MyFun函數的。

    return 0;
}

typedef的功能是定義新的類型。第一句就是定義了一種funP的類型,並定義這種類型為指向某種函數指針,這種函數以一個int為參數並返回void類型。后面就可以像使用int,char一樣使用funP了。


參考:

C語言--指針詳解

C語言指針詳解(經典,非常詳細)



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM