天天看點

C語言深度解剖讀書筆記(4.指針的故事) 本節知識點:

轉自 http://blog.csdn.net/mbh_1991/article/details/10382771

指針這一節是本書中最難的一節,尤其是二級指針和二維數組直接的關系。

本節知識點:

1.指針基礎,一張圖說明什麼是指針:

C語言深度解剖讀書筆記(4.指針的故事) 本節知識點:

2.跨過指針,直接去通路一塊記憶體:     隻要你能保證這個位址是有效的 ,就可以這樣去通路一個位址的記憶體*((unsigned int *)(0x0022ff4c))=10;  但是前提是 0x0022ff4c是有效位址。對于不同的編譯器這樣的用法還不一樣,一些嚴格的編譯器,當你定義一個指針,把這個指針指派為一個這樣的位址的時候,當檢測到位址無效,編譯的時候就會報錯!!!如果一些不太嚴格的編譯器,不管位址有效無效都會編譯通過,但是對于無效位址,當你通路這塊位址的時候,程式就會運作停止! 3. a     &a    &a[0]三者的差別: 首先說三者的值是一樣的,但是意義是不一樣的。(這裡僅僅粗略的說說,詳細見文章 <c語言中數組名a和&a>)      &a[0]:這個是數組首元素的位址      a : 的第一個意義是 數組首元素的位址,此時與&a[0]完全相同

                第二個意義是 數組名  sizeof(a)  為整體數組有多少個位元組     &a :這個是數組的位址 。跟a的差別就是,a是一個 int* 的指針(在第一種意義的時候) ,而&a是一個 int (*p)[5]類型的數組指針, 指針運算的結果不一樣。(此處的int* 僅僅是為了舉例子,具體應該視情況而定) 4. 指針運算(本節最重要的知識點,但并不是最難的,是以的問題都來源于這兒):       對于指針的運算,首先要清楚的是 指針類型( 在C語言中,資料的類型決定資料的行為),然後對于加減其實就是對這個指針的大小加上或者減去,n*sizeof(這個指針指向的資料的類型)。即:一個類型為T的指針的移動,是以sizeof(T)為機關移動的。如:int* p;  p+1就是p這個指針的值加上sizeof(int)*1,即:(unsigned int)p + sizeof(int)*1。對于什麼typedef的,struct的,數組的都是一樣的。 這個有一個例子,代碼如下: [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. int main(int argc, char *argv[])   
  4. {  
  5.     int a[5]={1,2,3,4,5};  
  6.     int* p=(int *)(&a+1);  
  7. //  int *p=&a+1;  //這個條語句是  把&a這個數組指針 進行了指針運算後  的那個位址  強制類型轉換成了 int *指針   
  8.     printf("%d\n",*(p-1));  
  9.     return 0;  
  10. }  

5.通路指針和通路數組的兩種方式:     分别是以下标方式通路和以指針的方式通路,我覺得沒有任何差別,*(p+4)和p[4]是一樣的 ,其實都可以了解成指針運算。如果非要說出差別,我覺得指針的方式會快些,但是在目前的硬體和編譯器角度看,不會太明顯。同樣下标的方式可讀性可能會高些。 6. 切記數組不是指針:

    數組是數組,指針是指針,根本就是兩個完全不一樣的東西。當然要是在宏觀的記憶體角度看,那一段相同類型的連續空間,可以說的上是數組。 但是你可以嘗試下,定義一個指針,在其他地方把他聲明成數組,看看編譯器會不會把兩者混為一談,反過來也不會。     但是為什麼我們會經常弄混呢?第一,我們常常利用指針的方式去通路數組。第二,數組作為函數參數的時候,編譯器會把它退化成為指針,因為函數的參數是拷貝,如果是一個很大的數組,拷貝是很浪費記憶體的,是以數組會被退化成指針( 這裡一定要了解好,退化的是數組成員的類型指針,不一定是數組指針的哈)。 7. 弄清數組的類型:    數組類型是由數組 元素類型和 數組長度兩個因素決定的,這一點在數組中展現的不明顯,在數組指針的使用中展現的很好。 [cpp]  view plain copy

  1. char a[5]={'a','b','c','d','e'};  
  2. char (*p)[3]=&a;  

   上面的代碼是錯誤的,為什麼?因為數組指針和數組不是一個類型,數組指針是指向一個數組元素為char 長度為3的類型的數組的,而這個數組的類型是數組元素是char長度是5,類型不比對,是以是錯的。 8.字元串問題:    a.C語言中沒有真正的字元串,是用字元數組模拟的,即:字元串就是以'\0'結束的字元數組。    b.要注意下strlen,strcmp等這個幾個函數的傳回值,是有符号的還是無符号的,這裡很容易忽略傳回值類型,造成操作錯誤。    c.使用一條語句實作strlen,代碼如下(此處注意assert函數的使用,安全性檢測很重要): [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <assert.h>  
  3. int strlen(const char* s)  
  4. {  
  5.     return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );  
  6. }  
  7. int main()  
  8. {  
  9.     printf("%d\n", strlen( NULL));  
  10.     return 0;  
  11. }  

    d.自己動手實作strcpy,代碼如下: [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <assert.h>  
  3. char* strcpy(char* dst, const char* src)  
  4. {  
  5.     char* ret = dst;  
  6.     assert(dst && src);  
  7.     while( (*dst++ = *src++) != '\0' );  
  8.     return ret;  
  9. }  
  10. int main()  
  11. {  
  12.     char dst[20];  
  13.     printf("%s\n", strcpy(dst, "hello!"));  
  14.     return 0;  
  15. }  

     e. 推薦使用strncpy、strncat、strncmp這類長度受限的函數(這些函數還能在字元串後面自動補充'\0'),不太推薦使用strcpy、strcmpy、strcat等長度不受限僅僅依賴于'\0'進行操作的一系列函數,安全性較低。

     f.補充問題,為什麼對于字元串char a[256] = "hello";,在printf和scanf函數中,使用a行,使用&a也行?代碼如下: [cpp]  view plain copy

  1. #include <stdio.h>  
  2. int main()  
  3. {  
  4.     char* p ="phello";  
  5.     char a[256] = "aworld";  
  6.     char b[25] = {'b','b','c','d'};  
  7.     char (*q)[256]=&a;  
  8.     printf("%p\n",a);  //0022fe48  
  9.     //printf("%p\n",&a);  
  10.     //printf("%p\n",&a[0]);  
  11.     printf("tian %s\n",(0x22fe48));   
  12.     printf("%s\n",q);    //q就是&a   
  13.     printf("%s\n",*q);   //q就是a   
  14.     printf("%s\n",p);  
  15.     printf("%s\n",a);  
  16.     printf("%s\n",&a);  
  17.     printf("%s\n",&a[0]);  
  18.     printf("%s\n",b);  
  19.     printf("%s\n",&b);  
  20.     printf("%s\n",&b[0]);     
  21. }  

對于上面的代碼:中的0x22fe48是根據列印a的值獲得的。 printf("tian %s\n",(0x22fe48));這條語句, 可以看出來printf真的是不區分類型啊,完全是根據%s來判斷類型。後面隻需要一個值,就是字元串的首位址。a、&a、&a[0]三者的值還恰巧相等,是以說三個都行,因為printf根本就不判斷指針類型。雖然都行但是我覺得要寫有意義的代碼, 是以最好使用a和*p。還有一個問題就是,char* p = "hello"這是一個char*指針指向hello字元串。是以對于這種方式隻能使用p。 因為*p是hello字元串的第一個元素,即:‘h’,&p是char* 指針的位址,隻有p是儲存的hello字元串的首位址,是以隻有p可以,其他都不可以。scanf同理,因為&a和a的值相同,且都是數組位址。

9. 二維數組(本節最重要的知識點):       a.對于二維數組來說,二維數組就是一個一維數組 數組,每一個數組成員還是一個數組,比如int a[3][3],可以看做3個一維數組,數組名分别是a[0]  a[1]   a[2]   sizeof(a[0])就是一維數組的大小  ,*a[0]是一維數組首元素的值,&a[0]是 一維數組的數組指針。       b.也可以通過另一個角度看這個問題。a是二維數組的數組名,數組元素分别是數組名為a[0]、a[1]、a[2]的三個一維數組。對a[0]這個數組來說,它的數組元素分别是a[0][0]  a[0][1]  、 a[0][2]三個元素。a和a[0]都是數組名,但是是兩個級别的,a作為數組首元素位址的時候等價于&a[0]( 最容易出問題的地方在這裡,這裡一定要弄清此時的a[0]是什麼,此時的a[0]是數組名,不是數組首元素的位址,不可以繼續等價下去了,千萬不能這樣想 a是&a[0]    a[0]是&a[0][0]     a就是&&a[0][0] 然後再弄個2級指針出來,自己就蒙了!!!這是一個典型的錯誤,首先&&a[0][0]就沒有任何意義,跟2級指針一點關系都沒有,然後a[0]此時不代表數組首元素位址,是以這個等價是不成立的。Ps:一定要搞清概念,很重要!!! ), a[0]作為數組首元素位址的時候等價于&a[0][0]。但是二維數組的數組頭有很多講究,就是a(二維數組名)、&a(二維數組的數組位址)、&a[0](二維數組首元素位址  即a[0]一維數組的數組位址 a有的時候也表示這個意思)、a[0](二維數組的第一個元素 即a[0]一維數組的數組名)、&a[0][0](a[0]一維數組的數組首元素的位址 a[0]有的時候也表示這個意思),這些值都是相等,但是他們類型不相同,行為也就不相同,意義也不相同。分析他們一定要先搞清,他們分别代表什麼。 下面是一個,二維數組中指針運算的練習(指針運算的規則不變,類型決定行為): [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4. int main(int argc, char *argv[])   
  5. {  
  6.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  7.     printf("%d\n",sizeof(a[0]));  
  8.     printf("%d\n",*a[2]);  
  9.     printf("%d\n",*(a[0]+1));  
  10.     printf("%p\n",a[0]);  
  11.     printf("%p\n",a[1]);  
  12.     printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一樣  指針類型不一樣   &a[0]+1這個是數組指針  a[1]是&a[1][0] 是int*指針   
  13.     printf("%d\n",*((int *)(&a[0]+1)));  
  14.     printf("%d\n",*(a[1]+1));  
  15.     printf("%p\n",a);  
  16.     printf("%p\n",&a);  
  17.     printf("%p\n",&a[0]);  
  18.     printf("%d\n",sizeof(a));   //這是a當作數組名的時候  
  19.     printf("%d\n",*((int *)(a+1))); //此時 a是數組首元素的位址  數組首元素是a[0]    
  20.                  //首元素位址是&a[0]  恰巧a[0]是數組名 &a[0]就變成了數組指針   
  21.     return 0;  
  22. }  

總結:對于a和a[0]、a[1]等這些即當作數組名,又當作數組首元素位址,有時候還當作數組元素(即使當作數組元素,也無非就是當數組名,當數組首元素位址兩種),這種特殊的變量, 一定要先搞清它現在是當作什麼用的。

      c.二維數組中一定要注意,大括号,還是小括号,意義不一樣的。 10. 二維數組和二級指針:      很多人看到二維數組,都回想到二級指針,首先我要說二級指針跟二維數組毫無關系,真的是一點關系都沒有。通過指針類型的分析,就可以看出來 兩者毫無關系。不要在這個問題上糾結。 二級指針隻跟指針數組有關系,如果這個二維數組是一個二維的指針數組,那自然就跟二級指針有關系了,其他類型的數組則毫無關系。切記!!!還有就是二級指針與數組指針也毫無關系!! 11. 二維數組的通路:      二維數組有以下的幾種通路方式:      int   a[3][3];對于一個這樣的二位數組      a.方式一:printf("%d\n",a[2][2]);       b.方式二:printf("%d\n",*(a[1]+1));      c.方式三:printf("%d\n",*(*(a+1)+1));      d.方式四:其實二維數組在記憶體中也是連續的,這麼看也是一個一維數組,是以就可以使用這個方式,利用數組成員類型的指針。 [cpp]  view plain copy

  1. int *q;  
  2. q = (int *)a;  
  3. printf("%d\n",*(q+6));  

     e.方式五:二維數組中是由多個一維數組組成的,是以就可以利用數組指針來通路二維數組。 [cpp]  view plain copy

  1. int (*p)[3];  
  2. p = a;  
  3. printf("%d\n",*(*(p+1)+1));  

給一個整體的程式代碼: [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <string.h>  
  4. int main()  
  5. {  
  6.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  7.     int (*p)[3];  
  8.     int *q;   
  9.     printf("%d\n",*(*(a+1)+1));   //a        *(&a[0]+1)  
  10.     p = a;  
  11.     q = (int *)a;  
  12.     printf("%d\n",*(*(p+1)+1));  
  13.     printf("%d\n",*(a[1]+1));  
  14.     printf("%d\n",a[1][1]);  
  15.     printf("%d\n",*(q+6));  
  16. }  
  17. <span style="font-family:Arial;BACKGROUND-COLOR: #ffffff"></span>  

  總結:對于二位數組int a[3][3]  要想定義一個指針指向這個二維數組的數組元素(即a[0]等一維數組),就要使用數組指針,這個數組指針要跟數組類型相同。a[0]等數組類型是元素類型是int,長度是3,是以數組指針就要定義成int (*p)[3]。後面的這個次元一定要比對上,不然的話類型是不相同的。 這裡有一個程式,要記得在c編譯器中編譯,這個程式能看出類型相同的重要性: [cpp]  view plain copy

  1. <span style="color:#000000;">#include <stdio.h>  
  2. int main()  
  3. {  
  4.     int a[5][5];  
  5.     int(*p)[4];  
  6.     p = a;  
  7.     printf("%d\n", &p[4][2] - &a[4][2]);  
  8. }</span>  

12. 二級指針:     a.因為指針同樣存在傳值調用和傳址調用,并且還有指針數組這個東西的存在,是以二級指針還是有它的存在價值的。     b.常使用二級指針的地方:           (1)函數中想要改變指針指向的情況,其實也就是函數中指針的傳址調用,如:重置動态空間大小,代碼如下: [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <malloc.h>  
  3. int reset(char**p, int size, int new_size)  
  4. {  
  5.     int ret = 1;  
  6.     int i = 0;  
  7.     int len = 0;  
  8.     char* pt = NULL;  
  9.     char* tmp = NULL;  
  10.     char* pp = *p;  
  11.     if( (p != NULL) && (new_size > 0) )  
  12.     {  
  13.         pt = (char*)malloc(new_size);  
  14.         tmp = pt;  
  15.         len = (size < new_size) ? size : new_size;  
  16.         for(i=0; i<len; i++)  
  17.         {  
  18.             *tmp++ = *pp++;        
  19.         }  
  20.         free(*p);  
  21.         *p = pt;  
  22.     }  
  23.     else  
  24.     {  
  25.         ret = 0;  
  26.     }  
  27.     return ret;  
  28. }  
  29. int main()  
  30. {  
  31.     char* p = (char*)malloc(5);  
  32.     printf("%0X\n", p);  
  33.     if( reset(&p, 5, 3) )  
  34.     {  
  35.         printf("%0X\n", p);  
  36.     }  
  37.     return 0;  
  38. }  

             (2)函數中傳遞指針數組的時候,實參(指針數組)要退化成形參(二級指針)。              (3)定義一個指針指向指針數組的元素的時候,要使用二級指針。       c.指針數組:char* p[4]={"afje","bab","ewrw"};  這是一個指針數組,數組中有4個char*型的指針,分别儲存的是"afje"、"bab"、"ewrw"3個字元串的位址。p是數組首元素的位址即儲存"afje"字元串char*指針的位址。 [cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4. int main(int argc, char *argv[])   
  5. {     
  6.     char* p[4]={"afje","bab","ewrw"};  
  7.     char* *d=p;   
  8.     printf("%s\n",*(p+1));    
  9.     printf("%s\n",*(d+1));  //d  &p[0] p[0]是"afje"的位址,是以&p[0]是儲存"afje"字元串的char*指針的位址      
  10.     return 0;  
  11. }  

       d.子函數malloc,主函數free,這是可以的(有兩種辦法,第一種是利用return 把malloc的位址傳回。第二種是利用二級指針,傳遞一個指針的位址,然後把malloc的位址儲存出來)。記住不管函數參數是,指針還是數組, 當改變了指針的指向的時候,就會出問題,因為子函數中的指針就跟主函數的指針不一樣了,他隻是一個複制品,但可以改變指針指向的内容。這個知識點可以看<在某教育訓練機構的聽課筆記>這篇文章。

13.數組作為函數參數:數組作為函數的實參的時候,往往會退化成數組元素類型的指針。如:int a[5],會退化成int*   ;指針數組會退化成二級指針;二維數組會退化成一維數組指針;三維數組會退化成二維數組指針(三維數組的這個是我猜得,如果說錯了,希望大家幫我指出來,謝謝)。如圖:

C語言深度解剖讀書筆記(4.指針的故事) 本節知識點:

二維數組作為實參的例子:

[cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4. int fun(int (*b)[3])  //此時的b為  &a[0]   
  5. {  
  6.     printf("%d\n",*(*(b+1)+0));  
  7.     printf("%d\n",b[2][2]);// b[2][2] 就是  (*(*(b+2)+2))  
  8.     printf("%d\n",*(b[1]+2));  
  9. }  
  10. int main(int argc, char *argv[])   
  11. {  
  12.     int a[3][3]={1,2,3,4,5,6,7,8,9};  
  13.      fun(a);//與下句話等價  
  14.      fun(&a[0]);      
  15.     return 0;  
  16. }  

       數組當作實參的時候,會退化成指針。指針當做實參的時候,就是單純的拷貝了!

14.函數指針與指針函數:

      a.對于函數名來說,它是函數的入口,其實函數的入口就是一個位址,這個函數名也就是這個位址。這一點用彙編語言的思想很容易了解。下面一段代碼說明函數名其實就是一個位址,代碼如下:

[cpp]  view plain copy

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdbool.h>  
  4. void abc()  
  5. {  
  6.     printf("hello fun\n");  
  7. }  
  8. int main(int argc, char *argv[])   
  9. {  
  10.     void (*d)();  
  11.     void (*p)();  
  12.     p = abc;  
  13.     abc();  
  14.     printf("%p\n",abc);  
  15.     printf("%p\n",&abc);//函數abc的位址0x40138c  
  16.     p();  
  17.     (*p)();       
  18.     d = ((unsigned int*)0x40138c);  //其實就算d= 0x40138c這麼給指派也沒問題   
  19.     d();  
  20.     return 0;  
  21. }     

可見函數名就是一個位址,是以函數名abc與&abc沒有差別,是以p和*p也沒有差別。

    b.我覺得函數指針最重要的是它的應用環境,如回調函數(其實就是利用函數指針,把函數當作參數進行傳遞)代碼如下,還有中斷處理函數(同理)詳細見<

ok6410學習筆記(16.按鍵中斷控制led)>中的 中斷注冊函數,request_irq。還有就是函數指針數組,第一次見到函數指針數組是在zigbee協定棧中。

回調函數原理代碼:

[cpp]  view plain copy

  1. #include <stdio.h>  
  2. typedef int(*FUNCTION)(int);  
  3. int g(int n, FUNCTION f)  
  4. {  
  5.     int i = 0;  
  6.     int ret = 0;  
  7.     for(i=1; i<=n; i++)  
  8.     {  
  9.         ret += i*f(i);  
  10.     }  
  11.     return ret;  
  12. }  
  13. int f1(int x)  
  14. {  
  15.     return x + 1;  
  16. }  
  17. int f2(int x)  
  18. {  
  19.     return 2*x - 1;  
  20. }  
  21. int f3(int x)  
  22. {  
  23.     return -x;  
  24. }  
  25. int main()  
  26. {  
  27.     printf("x * f1(x): %d\n", g(3, f1));  
  28.     printf("x * f2(x): %d\n", g(3, &f2));  
  29.     printf("x * f3(x): %d\n", g(3, f3));  
  30. }  

注意:可以使用函數名f2,函數名取位址&f2都可以,但是不能有括号。

       c.所謂指針函數其實真的沒什麼好說的,就是一個傳回值為指針的函數而已。

15.指派指針的閱讀:

       a.char* (*p[3])(char* d); 這是定義一個函數指針數組,一個數組,數組元素都是指針,這個指針是指向函數的,什麼樣的函數參數為char*  傳回值為char*的函數。

分析過程:char (*p)[3] 這是一個數組指針、char* p[3] 這是一個指針數組  char* 是數組元素類型、char* p(char* d) 這個是一個函數傳回值類型是char* 、char (*p)(char* d)這個是一個 函數指針。可見char* (*p[3])(char* d)是一個數組  數組中元素類型是 指向函數的指針,char* (* )(char* d) 這是函數指針類型,char* (* )(char* d) p[3] 函數指針數組 這個不好看 就放裡面了。(PS:這個看看就好了~~~當娛樂吧)

      b.函數指針數組的指針:char* (*(*pf)[3])(char* p) //這個就看看吧  我覺得意義也不大 因為這個邏輯要是一直下去 就遞歸循環了。

分析過程:char* (* )(char *p) 函數指針類型,char* (*)(char *p) (*p)[3]  函數指針 數組指針  也不好看 就放裡面了。

繼續閱讀