C語言二級指針的介紹和使用

        鋤禾日當午,學C真TM苦。

        指針是C語言中功能最強大的部分,但是也是最難理解的部分。

        本文主要介紹二級指針的使用,包括與數組和函數結合的使用,一級指針簡單帶過。

一、一級指針

一級指針的簡單描述

①一級指針是一種以存儲其他變量地址爲目的的變量。一個T類型的指針變量(一級指針)就是一個存儲了某T類型值變量的內存地址的引用。

②對指針的解引用(也就是*),是獲取指針指向的內存空間中存儲的值。

③當指針解引用做左值的時候,可以改變指針指向內存空間中存儲的值。

④指針指向哪個變量,就將該變量的內存地址賦給該指針(用於指針變量做左值的時候)。

⑤改變指針變量的值(指針變量做左值時),就改變了該指針的指向。

二、二級指針的相關介紹

        多級指針(pointer to pointer to)是指向指針的指針,二級指針是指向一級指針的指針。

一級指針指向的是某個變量,也就是一級指針中存儲的是某個變量的內存地址;二級指針指向一級指針,也就是二級指針中存儲的是一級指針的內存地址。

代碼

int main(void)
{
    int a = 10;                        //聲明一個變量a
    int *p = &a;                       //聲明指針p,指向變量a
    int **q = &p;                      //聲明二級指針q,指向一級指針p
    printf("a = %d\n",a);              //打印變量a的值
    printf("a的地址&a=%p\n",&a);        //打印變量a的地址
    printf("p = %p\n",p);              //打印p的值
    printf("p的地址&p=%p\n",&p);        //打印p的地址
    printf("p的解引用*p=%d\n",*p);      //打印p的解引用
    printf("q = %p\n",q);              //打印q的值
    printf("q的地址&q=%p\n",&q);        //打印q的地址
    printf("q的解引用*q=%p\n",*q);      //打印q的解引用
    printf("q的雙重解引用**q=%d\n",**q); //打印q的雙重解引用
    return 0;
}

執行結果

a = 10

a的地址&a=0x7fff5fbff838

p = 0x7fff5fbff838

p的地址&p=0x7fff5fbff830

p的解引用*p=10

q = 0x7fff5fbff830

q的地址&q=0x7fff5fbff828

q的解引用*q=0x7fff5fbff838

q的雙重解引用**q=10

內存結構示意圖

wKiom1Xz386jNEX9AAGufVXL4L4041.jpg

        如上圖的代碼和執行後的結果,從結果中就可以看出變量a,一級指針p和二級指針q的關係。

在學習的過程中主要是對二級指針不是很理解,所以這裏特別對二級指針說明一下。

變量q是一個二級指針,裏面存放的是一級指針p的地址,對q的解引用就得到了q所指向的一級指針的值,也就是變量a的地址,對q雙重解引用就得到了變量a的值,所以也可以通過二級指針來修改變量a的值。


三、指針與二位數組

        首先一點的是,雖然二維數組的數組名可以看做是一個指針,但是並不能將二維數組的數組名賦值給一個二級指針,也就是如下的代碼

int main(void)
{
    int arr[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
    int **p = arr;
    return 0;
}

        上面的代碼在執行的時候會報錯,這是因爲兩者數據類型是不相同的,所以不能賦值。

二維數組的數組名指向的是一維數組,也就是指向數組類型,但是二級指針指向的是一級指針,也就是指針類型,所以兩者不能互相賦值。


下面詳細介紹指針與二維數組的關係

聲明一個二維數組

int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

        要想理解指針和二維數組之間的關係,就要首先弄清楚各種數組名所代表的元素。

對於二維數組的存儲,是按照先行後列的形式排列的,把每一行看做是一個一位數組,那二維數組就是由多個一位數組組成的數組,即二維數組是數組的數組。

        對於二維數組名,可以將它看做是指向行數組的指針,也就是說二維數組的元素是行數組,所以對於二維數組加減的變化是以行數組的大小爲單位的,即arr指向arr[0]這個行數組,arr+1指向的是arr[1]這個行數組。對其進行解引用,得到的是每一行數組的首地址,即*arr表示的是第0行數組的首地址,和arr[0]相等,*(arr+1)表示的是第1行數組的首地址,和arr[1]是相等的。假如要取第1行第2個元素的地址,就是arr[1]+2,因爲此時arr[1]代表的是一維數組,所以它的元素就是一個數字,在加減的時候移動的就是元素的大小,例如+1就表示該數組中第1個元素,+3就表示數組中的3個元素(以0開始)。因爲

*(arr+1)和arr[1]相等,所以第1行第2個元素的地址也可以表示爲*(arr+1)+2,對這個地址進行解引用,得到的就是數組元素的具體值,也就是*(*(arr+1)+2)。

所以有如下公式,假如一個二維數組每行有N個元素,二維數組名是arr,那第i行第j個元素的地址是

*(arr+i*N)+j,也可以表示爲arr[i]+j。

元素的值可以表示爲*(*(arr+i)+j),或者可以表示爲arr[i][j]。

還用數組的形式來表示元素的地址和值,可以更方便編程人員的閱讀,但是使用指針的方式更方便與C語言編譯器閱讀。

驗證代碼如下

int main(void)
{
    int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
    //分別使用不同的形式打印第2行,第3個元素的值和地址
    //根據推到出來的規律如下
    //該元素的地址
    printf("%p\n",*(arr+2*4)+3);
    printf("%p\n",arr[2]+3);
    printf("%p\n",&arr[2][3]);
    //該元素的值是12
    printf("%d\n",*(*(arr+2)+3));
    printf("%d\n",arr[2][3]);
    return 0;
}

執行結果如下

0x7fff5fbff88c

0x7fff5fbff82c

0x7fff5fbff82c

12

12

        對上述二維數組arr,雖然arr[0]、arr都是數組首地址,但二者指向的對象不同,arr[0]是一維數組的名字,它指向的是arr[0]數組的首元素,對其進行“*”運算,得到的是一個數組元素值,即arr[0]數組首元素值,因此,*arr[0]與arr[0][0]是同一個值;而a是一個二維數組的名字,它指向的是它所屬元素的首元素,它的每一個元素都是一個行數組,因此,它的指針移動單位是“行”,所以arr+i指向的是第i個行數組,即指向arr[i]。對arr進行“*”運算,得到的是一維數組arr[0]的首地址,即*arr與arr[0]是同一個值。當用int *p;定義指針p時,p的指向是一個int型數據,而不是一個地址,因此,用arr[0]對p賦值是正確的,而用arr對p賦值是錯誤的。


四、數組指針

        既然不能將二位數組的數組名賦值給二位指針,那該用什麼來表示二位數組呢。答案就是數組指針。數組指針就是指向數組的指針。

    int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

    int (*p)[4] = arr;

        如上圖所示的聲明方式,可以認爲指針p就是指向數組的指針,這個數組中有4個int類型的元素,此時p的增量以它所指向的一維數組長度爲單位。同時要想使用指針p來表示數組arr中的元素,上面總結的規律也是可以使用的。

如下圖的代碼,就是將上面的arr換成了指針p,同樣可以使用。

int main(void)

{

    int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

    int (*p)[4] = arr;

    //分別使用不同的形式打印第2行,第3個元素的值和地址

    //根據推到出來的規律如下

    //該元素的地址

    printf("%p\n",*(p+2*4)+3);

    printf("%p\n",p[2]+3);

    printf("%p\n",&p[2][3]);

    //該元素的值是12

    printf("%d\n",*(*(p+2)+3));

    printf("%d\n",p[2][3]);

    return 0;

}

使用數組指針指向一維數組

int a[3]={1,2,3}; 那麼p就是指向這個數組a的指針。
int(*p)[3]=&a; // 這裏賦值一定要用取地址符號。也就是取數組a的地址。

不可以這樣賦值: int(*p)[3]=a; // error :類型不兼容。a本來是數組類型,是不可以賦值給int(*)[3]這個類型的。

但是這樣是可以的int *p1=a; // ok 因爲a可以隱式轉換爲int*類型,其值實際上就是數組第一個元素的地址,也就是&a[0]。


數組指針練習題

//打印數組
void print_Array(char (*p)[30],int *len)
{
    for(int i=0;i<*len;i++)
    {
        printf("%s\n",p[i]);
    }
}

//數組排序
int sort(char (*p)[30],int *len)
{
    int ret = 0;
    char tmp[100];
    if(p==NULL||len==NULL)
    {
        printf("function sort error");
        ret = -1;
    }
    for(int i=0;i<*len;i++)
    {
        for(int j=i+1;j<*len;j++)
        {
            if(strcmp(p[i],p[j])>0)
            {
                strcpy(tmp,p[i]);
                strcpy(p[i],p[j]);
                strcpy(p[j],tmp);
            }
        }
    }
    return ret;
}

int main(void)
{
    char array[10][30] = {"sdecs","codeq","owxja","qwer","bsdws"};
    int count = 5;
    print_Array(array, &count);
    sort(array, &count);
    printf("排序之後的數組\n");
    print_Array(array, &count);
    return 0;
}

執行結果

sdecs
codeq
owxja
qwer
bsdws
排序之後的數組
bsdws
codeq
owxja
qwer
sdecs

上面的代碼是使用數組指針做函數參數,接受主調函數傳遞過來的二維數組的地址,然後利用數組指針對二位數組進行相關的操作。



五、指針數組

        說完了數組指針,現在接着要說一下指針數組,這兩者並沒有什麼聯繫,放在一起是因爲兩者的聲明比較像,有時候容易弄混,放在一起可以比較一下。

        指針數組就是數組裏面存放的是指針這種數據類型。

int main(void)
{
    int a = 1;
    int b = 2;
    int c = 3;
    int *d = &a;
    int *e = &b;
    int *f = &c;
    int *p[3] = { d, e, f };
    printf("%d\n",*p[0]);
    printf("%d\n", *d);
    printf("%p\n", p[0]);
    printf("%p\n", &a);
    return 0;
}

執行結果

1

1

0x7fff5fbff818

0x7fff5fbff818

如上圖所示分別是指針數組的代碼和相應的執行結果。

上面的p就是一個數組,這個數組有3個元素,每個元素都是int *類型的,也就是每個元素都是指向int類型的指針。


下面是指針數組的練習題

//數組打印
void print_array(const char **p,const int *len)
{
    for(int i=0;i<*len;i++)
    {
        printf("%s\n",p[i]);
    }
}

//元素排序
int sort(char **p,int *len)
{
    char **str = p;
    int *count = len;
    char *tmp = NULL;
    //函數返回值
    int ret = 0;
    if(str==NULL||len==NULL)
    {
        printf("function sort error");
        return ret;
    }
    for(int i=0;i<*count;i++)
    {
        for(int j=i+1;j<*count;j++)
        {
            if(strcmp(str[i],str[j])>0)
            {
                tmp = str[i];
                str[i] = str[j];
                str[j] = tmp;
            }
        }
    }
    return ret;
}

int main(void)
{
    char *array[] = {"sdjd","asdjf","peroa","asoeq"};
    int count = sizeof(array)/sizeof(array[0]);
    //排序前打印
    print_array(array, &count);
    //調用排序函數
    sort(array, &count);
    //排序後打印
    printf("排序後打印\n");
    print_array(array, &count);
    return 0;
}

執行結果

sdjd
asdjf
peroa
asoeq
排序後打印
asdjf
asoeq
peroa
sdjd

在這裏需要補充的一點是,C語言中沒有字符串這種數據類型,字符串在C語言中使用字符數組來表示。

注意點:①在調用函數的時候,使用了二級指針接受main函數中的指針數組,同時可以看到在傳遞數組個數的時候仍然使用了實參傳遞地址,形參使用指針來接受參數的這種形式,這是指針在做函數參數的時候非常重要的應用。

在函數傳遞參數的時候,通過指針給主調函數中的變量間接複製,是指針存在最大的意義。例如想通過在被調函數中改變主調函數中變量的值,則可以在函數調用的時候將變量的地址傳遞過去,在被調函數中使用一級指針來接受,這樣在被調函數中就可以通過指針來間接改變變量的值;同理,如果在被調函數中要改變主調函數中一級指針的值,則可以在被調函數中通過二級指針接受一級指針的地址,從而實現在被調函數中改變一級指針的值。同時這樣做還有一個好處就是,因爲被調函數只能有一個返回值,當想返回多個值的時候就很難實現,但是通過上面的那種方式,即在被調函數中通過地址來間接改變變量(指針也是一種變量)值的方式,就可以在被調函數中改變多個變量的值。而只是讓函數返回函數執行狀態的值,這樣就可以判斷出現了哪種情況。


注意上面這段代碼(1代碼)和之前的數組指針練習題代碼(2代碼)的比較

①在計算數組元素個數的時候,2代碼如果也使用和1代碼相同的方式求元素個數,得到的結果永遠都是10,但實際元素個數並不是10,出現這種現象的原因是因爲2代碼中固定了數組元素的個數,但是1代碼中並沒有固定。

②在排序交換數組元素的時候,使用的方式也不相同,1代碼使用的是交換指針指向的方法,也就是說,指針中存儲的數組的地址改變了,指針的指向變了;2代碼使用的是交換交換數組的值,也就是指針指向不變,但是指針所指向的內存空間中的值變了。


六、三級指針形參接受二級指針地址進行相關操作

//排序
int sort(char ***p,int *len)
{
    char *tmp = NULL;
    int ret = 0;
    if(p==NULL||len==NULL)
    {
        printf("function sort error");
        ret = -1;
    }
    
    for(int i=0;i<*len;i++)
    {
        for(int j=i+1;j<*len;j++)
        {
            if((*p)[i]<(*p)[j])
            {
                tmp = (*p)[i];
                (*p)[i] = (*p)[j];
                (*p)[j] = tmp;
            }
        }
    }
    return ret;
}
//給二級指針分配內存
int getMem(char ***p,int count)
{
    int ret = 0;
    if(p==NULL)
    {
        printf("function getMem error");
        ret = -1;
    }
    *p = (char **)malloc(sizeof(char*)*(count));
    for(int i=0;i<count;i++)
    {
        (*p)[i] = (char *)malloc(sizeof(char)*100);
        sprintf((*p)[i],"%d%d%d",i,i,i);
    }
    return ret;
}
//打印數組
void printArray(char ***p,int count)
{
    for(int i=0;i<count;i++)
    {
        printf("%s\n",(*p)[i]);
    }
}
//釋放內存空間
int freePoint(char ***p,int count)
{
    if(p==NULL)
    {
        return -1;
    }
    for(int i=0;i<count;i++)
    {
        free((*p)[i]);
    }
    free(*p);
    return 0;
}

int main(void)
{
    char **p = NULL;
    int count = 5;
    //分配內存
    getMem(&p, count);
    //打印數組
    printArray(&p, count);
    //排序
    sort(&p, &count);
    //打印數組
    printArray(&p, count);
    //釋放內存
    freePoint(&p, count);
    return 0;
}

如上圖所示的代碼,在main方法中聲明瞭一個二級指針,然後先講二級指針的地址做實參傳遞給甘薯getMem,在函數getMem中使用三級指針做形參接受二級指針,在函數中分配相應的內存空間,然後賦值。

代碼注意點①在分配內存的時候,判斷的是三級指針p是否爲NULL,而不是二級指針

②在(*p)[i]的時候注意符號的優先級,上面的代碼如果改成*p[i]就會出錯,因爲[]的優先級大於*,所以這樣寫會造成越界。


剛學C語言,能力有限,以上的內容有不對或者寫的不好的地方請提出來,我會盡力改正。謝謝!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章