指針綜合篇

一、指針

1、指針的概念:用來保存地址的“變量”叫做指針,可以理解成指針是地址的一個別名。


例:定義一個整形指針

spacer.gif

wKiom1cfdxHw4FQsAAA37X6Ri0A666.png


2、“指針的內容”,“指針所指向的內容”,“指針變量的地址”

wKiom1cfdybjUbapAACjM92HHMQ499.png

spacer.gif

  (1)、指針的內容:

    指針變量p裏面存放的是a的地址,也就是0x0018ff44.

  (2)、指針所指向的內容:

    指針變量p裏面存放的地址(0x18ff44)這塊空間所對應的值,也就是10,我們通過*p(解引用)可以訪問到這個值。即:*p作爲右值時,*p==10,當*p作爲左值時代表的就是a這塊空間。

  (3)、指針的地址

    指針p本身有一個地址,由編譯器分配的我們不知道,注意:不要講指針變量p本身的地址和p所保存的地址(0x0018ff44)相混淆。


3、"未初始化的指針"和"NULL指針"

例:定義兩個指針變量p1和p2:

int *p1;              //未初始化的指針

int *p2=NULL;         //空指針

*p1=10;

*p2=10;

這樣賦值顯然是錯誤的,爲什麼呢???

  試想一下,p1,p2是一個指針變量,所以p1,p2裏面應該存放的應該是一個地址。而對於p1我們沒有往裏面放地址,這時p1是隨機指向的。如果此時解引用*p1,訪問到的是塊隨機的地址,再修改這個隨機地址裏面的值,假設這個隨機空間裏面的值非常重要,那你就再也找不回來了,所以通常定義一個指針就將他初始化爲NULL。

對於p2:雖然我們將它初始化爲NULL,但它指向的是一塊空地址啊!!!向一塊空地址裏面放東西,根本是不可能的。


4、指針常量:

例:*(int *)200=10;

相信很多人對這個表達是都會產生疑惑,其實很好理解的,200是一個常數,我們將它強制轉化爲 int *(也就是將常數200轉化成一個整形地址),再對這塊地址進行*引用,就訪問到一塊空間,可以對這塊空間進行賦值。


5、常量指針

例:int *p=&100;

讓指針指向一個常量,一般用不到。


二、高級指針


1、二級指針概念:

  什麼是指針???存放地址的變量就叫做指針,所以二級指針就是存放一級指針地址的指針變量。

wKiom1cfd2rCXgJrAAA5oj3837c189.png

spacer.gif

注意:跟一級指針一樣,二級指針變量p2裏面存放的是一級指針變量p1的地址,一級指針變量p1裏面存放的是a的地址。要想訪問一塊地址裏面的內容可以使用間接訪問符“*”,所以:

*p2==&p1, *p2就是訪問p2這塊空間裏面存放的地址所對應的內容。

**p2==10,因爲*p2得到的結果是p1的地址,在對p1的地址進行解引用*p1就訪問到了10.


  例1:分析下面幾種情況下能不能作爲可修改的左值(可修改的左值必須代表一塊空間)

int a=10;

int *cp=&a;


  (1)*cp=20 //可以作爲左值,當cp指向a時,*cp放到等號左邊代表a這塊空間,當*cp放到等號右邊代表a這塊空間的值。


  (2)&a=20  //錯誤,&a不可以作爲左值,因爲他不能表示一塊特定的空間,&a得到的結果是a的地址,但並不代表a這塊空間,要想使用這塊空間,必須進行*引用,*&a=20正確。&a可以作爲右值,代表是a的地址這個數值。


  (3)*cp+1     //不可以作爲左值,因爲*優先級高於+,所以*cp先結合,再加1相當於10+1,不代表一塊空間。


  (4)*(cp+1)   //可以作爲左值,cp+1先結合,指向a的下一塊空間,再對這塊空間進行*引用,放在等號左邊就代表這塊空間。


  (5)++cp  //不可以作爲左值,因爲++cp只是將cp裏面的內容加一,並沒有進行*引用


  (6)cp++  //不可以作爲左值


  (7)*cp++   //可以作爲左值,先對cp裏面的地址進行*引用。再讓cp=cp+1(也就是讓cp指向a的下一塊空間,因爲++優先級高於*)


  (8)++*cp   //不可以作爲左值,*cp代表cp所指向的內容,再讓其加一,相當於10+1

注意:C中++cp和--cp都不能做左值,C++中++cp可以作爲左值。


  例2:const 修飾一級指針,修飾二級指針(const修飾的變量,還是一個變量,只不過只是可讀的 

int const a=10;

int b=30;

  1、a=20;   //錯誤,const修飾a,所以不能改變a的值


  2、int const *p; //const修飾的是*p,所以不能改變*p

    p=&a;       //正確  

    *p=20;      //錯誤 不能通過*p改變a的值


  3、const int *p;    //const修飾的是*p

    p=&a;       //正確      

    *p=20;      //錯誤


  4、int *const p=&a;    //const修飾的是p

     p=&b;          //錯誤   不能改變p

     *p=20;         //正確     


  5、int const * const p;  //const修飾*p,也修飾p,所以*p,p都不能改變

     p=&b;        //錯誤     

     *p=20;       //錯誤

注意const修飾變量的原則是離誰近修飾誰。const int *p與int const *p完全一樣。


2、指針和數組的關係 ,指針數組,數組指針,指針的運算


  2.1、指針和數組的關係:

    很多人都分不清指針和數組之間的關係,嚴格的來講指針和數組之間沒關係,指針是指針,數組是數組。只不過他們兩個都可以通過“*”引用的方式和下標的方式來訪問元素而已。


例1:

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

int *p=a;

  a[5]佔20個字節的大小,而p只佔4個字節的大小,其次p本身有自己的地址,只不過他裏面存放的是數組首元素的地址。

要訪問3則有兩種方式:a[2]或者*(a+2).

  其中*(a+2)就是*的形式訪問的,因爲a表示首元素的地址,加2表示向後偏移2個整形大小,找到3的地址,在通過*得到3.

在編譯器中a[2]會被先解析成*(a+2)訪問的。


例2:

wKioL1cfeOvAekVuAAC6r3lQoAM257.png所以必須保持定義和聲明的一致性,指針就是指針,數組就是數組。


3、指針數組,數組指針

spacer.gifwKiom1cfeFXidtNwAABQmND7fSo334.png

注意:[]的優先級高於*,指針數組是一個數組,只不過裏面的元素全部都是指針。數組指針是一個指針,指向數組的指針,偏移的單位是整個數組。


例1:

int a[6]={1,2,3,4,5,6};

int (*p2)[6];

p2=a;

  這是錯誤的,因爲指針p2的類型是int [6],所以應該是p2=&a;

int (*p2)[3];

  這樣的話p2的類型是int [3],所以p2=(int(*) [3])&a;  要強制轉換成數組指針的類型

注意:數組指針“所指向”的類型就是去掉指針變量之後所剩下的內容。

數組指針的類型就是去掉指針後剩下的內容。


例2:int (*p3)[5];

  p3的類型是 int(*)[5];

  p3所指向的類型是 int [5];


4、指針的運算:

   1、指針相減得到的結果是兩指針之間元素的個數,而不是字節數。

   2、指針的運算

例1:

struct test

{

int name;

char *pcname;

short data;

char c[2];

}*p;

   假設p的值是0x100000,結構體的大小是12;

則:

  1、 p+0x1=0x10000c    //p指向結構體 則p+1表示的是p+sizeof(test)*1


  2、(unsigned int)p+0x1=0x100001     //p已經被轉化爲一個無符號的數,加一就相當於兩個數相加


  3、(int *)p+0x1=0x100004            //p被轉化爲int *,所以p+1就表示p+sizeof(int *)*1


例2:假設當前機器小端存儲

   int a[4] = { 1, 2, 3, 4 };

   int *p = (int *)(a + 1);

   int *p1 = (int *)(&a + 1);

     int *p2 = (int *)(( int)&a + 1);

     printf( "*p=%d,*p1=%d,*p2=%d\n" , *p, p1[-1],*p2);


     輸出結果:*p=2,*p1=4,*p2=2000000 

解析:p = (int *)(a + 1); a代表首元素地址,加1指向第二個元素,所以是2.

     p1 = (int *)(&a + 1); &a表示數組的地址,&a+1就相當於&a+sizeof(a),p1指向的就是a[3]後面的地址,p1[-1]被解析成*(p1-1),所以是4.

     p2 = (int *)(( int)&a + 1); (int)&a是把數組的地址取出來再轉化爲一個int類型的數再加1,這時的加1就相當於兩個數相加(效果相當於讓&a向後偏移一個字節),相加的結果再轉化成(int *)地址,使p2指向這個地址。

     當前數組的存儲:01 00 00 00 02 00 00 00 ........... 因爲&a指向的是01這個字節的位置,(int)&a+1的結果是指向了01的下一個字節,這時在強制類型轉換成int *,則p2這時指向的空間的內容就是 00 00 00 02,因爲是小端存儲,所以大端就是 02 00 00 00,輸出後就是2000000.


5、二維數組與二級指針參數

(1)、二維數組做參數:

   二維數組做參數與一維數組做參數一樣,傳遞的都是首元素的地址,只不過二維數組的每個元素又是一個一維數組 。

例:int arr[5][10];

   這是一個5行10列的整形數組,可以將它看成一個只有5個元素的一維數組,只不過每個元素又是一個大小爲10的一維數組

   我們來分析一下當arr作爲實參是,需要什麼類型的形參來接受它???


   arr作爲參數時,代表首元素地址,要接受這個地址必須是一個指針或者是一個數組,但是他的每個元素的類型是int[10]。

   所以我們可以用一個指向類型爲int [10]的數組指針來接受arr[5][10],即:int (*p)[10] 。    

   同樣也可以用一個數組來接受arr,即:int arr1[][10],這裏第二維的大小不能省略,在這裏int arr1[][10] 也可以被解析成int (*arr1)[10];


(2)、二級指針做參數:

   例:char *p[5];

    這是一個大小爲5,每個元素都是char *類型的數組,當他作爲實參時,需要什麼樣類型的形參來接受他呢???

    分析:既然p是一個數組,那麼數組在作爲實參時,實際上傳遞過去的是首元素的地址,但是p的每一個元素都是一個char *類型,也是一個地址,所以形參的類型應該是char **。


總結:

spacer.gifwKiom1clqGqSbiKWAABFE00QjMY890.png

   數組名只有在sizeof()和取&時纔不發生降級,其餘地方都代表首元素地址。


6、函數指針


(1)函數指針: 是一個指向函數的指針

聲明:  

   例:void (*p)();           首先p是一個指針,它指向一個返回值爲空,參數爲空的函數。

初始化:

    要明確,函數指針本質還是一個指針,既然是指針,那麼就必須給他進行初始化才能使用它,下面來看一看函數指針的初始化,定義一個返回值爲空參數爲空的函數:void fun()。

        p=fun;

        p=&fun;

    這兩種初始化的方式都是正確的。因爲函數名在被編譯後其實就是一個地址,所以這兩種方式本質上沒有什麼區別。

調用:

    p();  

   (*p)();

    這兩種方式都是正確的。p裏面存的fun的地址,而fun與&fun又是一樣的。


例:分析一下(*(void (*) () ) 0 ) ()是個什麼東東!!!

   首先,void (*) ();是一個返回值爲空,參數爲空的函數指針類型。

   void (*) () 0 ;   它的作用是把0強制類型轉換成一個返回值爲空,參數爲空的函數指針。

   (*(void (*) () )0) ; 找到保存在0地址處的函數。

   (*(void (*) () ) 0 ) (); 對這個函數進行調用。


用途:

       1、回調函數:用戶將一個函數指針作爲參數傳遞給其他函數,後者再“回調”用戶的函數,這種方式稱爲“回調函數”,如果想要你編寫的函數在不同的時候執行不同的工作,這時就可以使用回調函數。回調函數也算是c語言裏面爲數不多的一個高大上的東西。

        2、轉換表:轉換表本質上是一個函數指針數組,說白了就是一個數組,只不過數組裏面存放的全部都是函數指針。


例:實現一個計算器

要求:有“+、-、*、/”四個功能。用戶輸入操作數,得出結果。

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int Add(int a, int b)
{
  return a + b;
}
int Sub(int a, int b)
{
 return a - b;
}
int Mul(int a, int b)
{
 return a *b;
}
int Div(int a, int b)
{
 assert(b != 0);
 return a / b;
}
int operator(int (*fun)(int,int))   //回調函數
{
  int a = 0;
  int b = 0;
  printf( "請輸入操作數:" );
  scanf( "%d%d", &a, &b);
  return (*fun )(a,b);              
}
int main()
{
  printf( "*************************************\n" );
  printf( "*0.exit           1.Add****\n" );
  printf( "*2.Sub           3.Mul****\n" );
  printf( "*4.Div           *********\n" );
  int(*fun[5])(int ,int);         //轉換表
  fun[1] = Add;
  fun[2] = Sub;
  fun[3] = Mul;
  fun[4] = Div;
  int input = 1;
  while (input)
  {
    printf( "請選擇> " );
    scanf( "%d", &input);
    if (input<0 || input>4)
    {                                             printf( "選擇無效\n" );
    }
    else if (input == 0)
    {                                                    break;
    }
    else
    {                                              int ret = operator(fun[input]);
      printf( "%d\n", ret);
    }
  }
   system( "pause");
   return 0;
}



在這個計算器程序中,就使用到了轉換表fun[]。fun[]裏面存放了加減乘除四個函數,根據input的不同,fun[input]調用不同的函數。這種方式與switch() case的功能比較相似,不過轉換表比switch()case更簡單。


(2)、函數指針數組:

       函數指針數組是一個指針數組,也就是一個數組,只不過裏面存放的全都是函數指針。

聲明:例: char* (*p[5])(int,int);

       p與[5]先結合成一個數組,所以這是一個大小爲5的數組,數組元素的類型是一個函數指針,這個函數指針指向一個返回值爲char *,有兩個參數,且參數類型爲int,int的數組。

可以這樣理解:char * (*)(int,int)   p[5];        其中char*(*)(int int)是p[5]的類型。


(3)、函數指針數組的指針:

       函數指針數組的指針是一個數組指針,本質上是一個指針,只不過指向的是一個存放函數指針的數組。

聲明:例:char *(*(*p)[5])(int,int);

        *與p先結合成一個指針,所以p是一個指針,指針所指向的是一個大小爲5的數組,這個數組存放的是函數指針,這些函數指針所指向的類型是返回值爲char *,有兩個int型參數的函數。

        可以這樣理解: char *(*[5])(int,int) *p;    p是一個指針,類型是char*(*[5])(int,int).


總結:指針數組,是一個數組,裏面存放的是指針。

數組指針,是一個指針,指向一個數組的指針。

函數指針數組,是一個數組,裏面存放的是函數指針。

函數指針數組指針,是一個指針,指向一個存放函數指針的數組。

其實規律很簡單,強調的都是最後兩個字,最後兩個字是什麼他就是什麼,而前面的字就是他的類型。



三、求內存和長度的差異:

1、整形數組:

int a[] = { 1, 2, 3, 4 };

printf( "%p\n",a);          //a代表首元素地址

printf( "%p\n",a+1);  //a+1表示第二個元素的地址

printf( "%p\n",&a);      //&a代表整個數組的首地址

printf( "%p\n",&a+1);  //&a代表數組的地址,&a+1則跳過整個數組

printf( "%d\n", sizeof (a));         //a在sizeof()中代表整個數組,    16  

printf( "%d\n", sizeof (a + 0));   //代表&a[0],32位下所有的地址大小都爲4                4  

printf( "%d\n", sizeof (*a));     //表示a[0],int佔4個字節                   4

printf( "%d\n", sizeof (a + 1));    //表示&a[1],32位下所有的地址大小都爲4             4  

printf( "%d\n", sizeof (a[1]));      //表示a[1],int佔4個字節                 4

printf( "%d\n", sizeof (&a));        //代表整個數組的地址,32位下所有的地址大小都爲4    4

printf( "%d\n", sizeof (&a + 1));     //因爲&a代表整個數組地址,&a+1跳過整個數組,但還是一個地址       

printf( "%d\n", sizeof (&a[0]));       //表示a[0]的地址        4

printf( "%d\n", sizeof (&a[0] + 1)); //表示a[1]的地址        4

printf( "%d\n", sizeof (*&a));         //&a代表整個數組,再*引用訪問這塊地址,就相當於求取真個數組的大小                   16 


2、字符數組:

char name[] = "abcdef" ;                          //這時字符串不代表首元素地址,而是字符數組的一種初始化方式,並且字符串總是默認以’\0‘結尾

printf( "%d\n", sizeof (name[0]));        //表示a,32位下char大小爲1         1

printf( "%d\n", sizeof (&name));          //表示整個數組的地址,32位下地址就是4          4

printf( "%d\n", sizeof (*name));           //表示 a                                  1

printf( "%d\n", sizeof (&name+1));      //表示指向整個數組之後的一塊空間,但還是一個地址  4

printf( "%d\n", sizeof (name+1));         //表示b的地址            4

printf( "%d\n", sizeof (name));              //代表整個數組,還要加上‘\0'      7      

printf( "%d\n", strlen(name));                //求取字符串長度,不包括’\0‘      6

printf( "%d\n", strlen(&name));             //代表整個數組的地址,&name==name,strlen遇到’\0‘停下                          6

printf( "%d\n", strlen(&name + 1));       //是一個隨機值,表示指向整個數組之後的一個地址,從這個地址開始向後尋找'\0',因爲’\0‘位置不確定所以是隨機值

printf( "%d\n", strlen(name + 1));         //表示b的地址


3、字符指針:

char *name = "abcdef" ;                      //字符串放在等號右邊代表首元素地址

printf( "%d\n", sizeof (name[0]));        //以下標形式訪問,代表 a         1

printf( "%d\n", sizeof (&name));         //表示字符指針name的地址        4

printf( "%d\n", sizeof (*name));          //通過*訪問,表示a                    1

printf( "%d\n", sizeof (&name+1));      //表示指針name之後的一塊地址      4

printf( "%d\n", sizeof (name+1));          //表示b的地址                                 4

printf( "%d\n", sizeof (name));              //代表a的地址,                          4

printf( "%d\n", strlen(name));                //代表字符串首元素地址            6

printf( "%d\n", strlen(&name));             //表示指針name本身地址,因爲從name開始向後’\0‘的位置不確定,所以是個隨機值

printf( "%d\n", strlen(&name + 1));         //表示指針name本身地址之後的地址,因爲’\0‘的位置不確定,所以是個隨機值

printf( "%d\n", strlen(name + 1));             //代表b的地址                   5






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