轉自 http://blog.csdn.net/mbh_1991/article/details/10382771
指針這一節是本書中最難的一節,尤其是二級指針和二維數組直接的關系。
本節知識點:
1.指針基礎,一張圖說明什麼是指針:

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
- #include <stdio.h>
- #include <stdlib.h>
- int main(int argc, char *argv[])
- {
- int a[5]={1,2,3,4,5};
- int* p=(int *)(&a+1);
- // int *p=&a+1; //這個條語句是 把&a這個數組指針 進行了指針運算後 的那個位址 強制類型轉換成了 int *指針
- printf("%d\n",*(p-1));
- return 0;
- }
5.通路指針和通路數組的兩種方式: 分别是以下标方式通路和以指針的方式通路,我覺得沒有任何差別,*(p+4)和p[4]是一樣的 ,其實都可以了解成指針運算。如果非要說出差別,我覺得指針的方式會快些,但是在目前的硬體和編譯器角度看,不會太明顯。同樣下标的方式可讀性可能會高些。 6. 切記數組不是指針:
數組是數組,指針是指針,根本就是兩個完全不一樣的東西。當然要是在宏觀的記憶體角度看,那一段相同類型的連續空間,可以說的上是數組。 但是你可以嘗試下,定義一個指針,在其他地方把他聲明成數組,看看編譯器會不會把兩者混為一談,反過來也不會。 但是為什麼我們會經常弄混呢?第一,我們常常利用指針的方式去通路數組。第二,數組作為函數參數的時候,編譯器會把它退化成為指針,因為函數的參數是拷貝,如果是一個很大的數組,拷貝是很浪費記憶體的,是以數組會被退化成指針( 這裡一定要了解好,退化的是數組成員的類型指針,不一定是數組指針的哈)。 7. 弄清數組的類型: 數組類型是由數組 元素類型和 數組長度兩個因素決定的,這一點在數組中展現的不明顯,在數組指針的使用中展現的很好。 [cpp] view plain copy
- char a[5]={'a','b','c','d','e'};
- char (*p)[3]=&a;
上面的代碼是錯誤的,為什麼?因為數組指針和數組不是一個類型,數組指針是指向一個數組元素為char 長度為3的類型的數組的,而這個數組的類型是數組元素是char長度是5,類型不比對,是以是錯的。 8.字元串問題: a.C語言中沒有真正的字元串,是用字元數組模拟的,即:字元串就是以'\0'結束的字元數組。 b.要注意下strlen,strcmp等這個幾個函數的傳回值,是有符号的還是無符号的,這裡很容易忽略傳回值類型,造成操作錯誤。 c.使用一條語句實作strlen,代碼如下(此處注意assert函數的使用,安全性檢測很重要): [cpp] view plain copy
- #include <stdio.h>
- #include <assert.h>
- int strlen(const char* s)
- {
- return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );
- }
- int main()
- {
- printf("%d\n", strlen( NULL));
- return 0;
- }
d.自己動手實作strcpy,代碼如下: [cpp] view plain copy
- #include <stdio.h>
- #include <assert.h>
- char* strcpy(char* dst, const char* src)
- {
- char* ret = dst;
- assert(dst && src);
- while( (*dst++ = *src++) != '\0' );
- return ret;
- }
- int main()
- {
- char dst[20];
- printf("%s\n", strcpy(dst, "hello!"));
- return 0;
- }
e. 推薦使用strncpy、strncat、strncmp這類長度受限的函數(這些函數還能在字元串後面自動補充'\0'),不太推薦使用strcpy、strcmpy、strcat等長度不受限僅僅依賴于'\0'進行操作的一系列函數,安全性較低。
f.補充問題,為什麼對于字元串char a[256] = "hello";,在printf和scanf函數中,使用a行,使用&a也行?代碼如下: [cpp] view plain copy
- #include <stdio.h>
- int main()
- {
- char* p ="phello";
- char a[256] = "aworld";
- char b[25] = {'b','b','c','d'};
- char (*q)[256]=&a;
- printf("%p\n",a); //0022fe48
- //printf("%p\n",&a);
- //printf("%p\n",&a[0]);
- printf("tian %s\n",(0x22fe48));
- printf("%s\n",q); //q就是&a
- printf("%s\n",*q); //q就是a
- printf("%s\n",p);
- printf("%s\n",a);
- printf("%s\n",&a);
- printf("%s\n",&a[0]);
- printf("%s\n",b);
- printf("%s\n",&b);
- printf("%s\n",&b[0]);
- }
對于上面的代碼:中的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
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int main(int argc, char *argv[])
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- printf("%d\n",sizeof(a[0]));
- printf("%d\n",*a[2]);
- printf("%d\n",*(a[0]+1));
- printf("%p\n",a[0]);
- printf("%p\n",a[1]);
- printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一樣 指針類型不一樣 &a[0]+1這個是數組指針 a[1]是&a[1][0] 是int*指針
- printf("%d\n",*((int *)(&a[0]+1)));
- printf("%d\n",*(a[1]+1));
- printf("%p\n",a);
- printf("%p\n",&a);
- printf("%p\n",&a[0]);
- printf("%d\n",sizeof(a)); //這是a當作數組名的時候
- printf("%d\n",*((int *)(a+1))); //此時 a是數組首元素的位址 數組首元素是a[0]
- //首元素位址是&a[0] 恰巧a[0]是數組名 &a[0]就變成了數組指針
- return 0;
- }
總結:對于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
- int *q;
- q = (int *)a;
- printf("%d\n",*(q+6));
e.方式五:二維數組中是由多個一維數組組成的,是以就可以利用數組指針來通路二維數組。 [cpp] view plain copy
- int (*p)[3];
- p = a;
- printf("%d\n",*(*(p+1)+1));
給一個整體的程式代碼: [cpp] view plain copy
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- int (*p)[3];
- int *q;
- printf("%d\n",*(*(a+1)+1)); //a *(&a[0]+1)
- p = a;
- q = (int *)a;
- printf("%d\n",*(*(p+1)+1));
- printf("%d\n",*(a[1]+1));
- printf("%d\n",a[1][1]);
- printf("%d\n",*(q+6));
- }
- <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
- <span style="color:#000000;">#include <stdio.h>
- int main()
- {
- int a[5][5];
- int(*p)[4];
- p = a;
- printf("%d\n", &p[4][2] - &a[4][2]);
- }</span>
12. 二級指針: a.因為指針同樣存在傳值調用和傳址調用,并且還有指針數組這個東西的存在,是以二級指針還是有它的存在價值的。 b.常使用二級指針的地方: (1)函數中想要改變指針指向的情況,其實也就是函數中指針的傳址調用,如:重置動态空間大小,代碼如下: [cpp] view plain copy
- #include <stdio.h>
- #include <malloc.h>
- int reset(char**p, int size, int new_size)
- {
- int ret = 1;
- int i = 0;
- int len = 0;
- char* pt = NULL;
- char* tmp = NULL;
- char* pp = *p;
- if( (p != NULL) && (new_size > 0) )
- {
- pt = (char*)malloc(new_size);
- tmp = pt;
- len = (size < new_size) ? size : new_size;
- for(i=0; i<len; i++)
- {
- *tmp++ = *pp++;
- }
- free(*p);
- *p = pt;
- }
- else
- {
- ret = 0;
- }
- return ret;
- }
- int main()
- {
- char* p = (char*)malloc(5);
- printf("%0X\n", p);
- if( reset(&p, 5, 3) )
- {
- printf("%0X\n", p);
- }
- return 0;
- }
(2)函數中傳遞指針數組的時候,實參(指針數組)要退化成形參(二級指針)。 (3)定義一個指針指向指針數組的元素的時候,要使用二級指針。 c.指針數組:char* p[4]={"afje","bab","ewrw"}; 這是一個指針數組,數組中有4個char*型的指針,分别儲存的是"afje"、"bab"、"ewrw"3個字元串的位址。p是數組首元素的位址即儲存"afje"字元串char*指針的位址。 [cpp] view plain copy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int main(int argc, char *argv[])
- {
- char* p[4]={"afje","bab","ewrw"};
- char* *d=p;
- printf("%s\n",*(p+1));
- printf("%s\n",*(d+1)); //d &p[0] p[0]是"afje"的位址,是以&p[0]是儲存"afje"字元串的char*指針的位址
- return 0;
- }
d.子函數malloc,主函數free,這是可以的(有兩種辦法,第一種是利用return 把malloc的位址傳回。第二種是利用二級指針,傳遞一個指針的位址,然後把malloc的位址儲存出來)。記住不管函數參數是,指針還是數組, 當改變了指針的指向的時候,就會出問題,因為子函數中的指針就跟主函數的指針不一樣了,他隻是一個複制品,但可以改變指針指向的内容。這個知識點可以看<在某教育訓練機構的聽課筆記>這篇文章。
13.數組作為函數參數:數組作為函數的實參的時候,往往會退化成數組元素類型的指針。如:int a[5],會退化成int* ;指針數組會退化成二級指針;二維數組會退化成一維數組指針;三維數組會退化成二維數組指針(三維數組的這個是我猜得,如果說錯了,希望大家幫我指出來,謝謝)。如圖:
二維數組作為實參的例子:
[cpp] view plain copy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int fun(int (*b)[3]) //此時的b為 &a[0]
- {
- printf("%d\n",*(*(b+1)+0));
- printf("%d\n",b[2][2]);// b[2][2] 就是 (*(*(b+2)+2))
- printf("%d\n",*(b[1]+2));
- }
- int main(int argc, char *argv[])
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- fun(a);//與下句話等價
- fun(&a[0]);
- return 0;
- }
數組當作實參的時候,會退化成指針。指針當做實參的時候,就是單純的拷貝了!
14.函數指針與指針函數:
a.對于函數名來說,它是函數的入口,其實函數的入口就是一個位址,這個函數名也就是這個位址。這一點用彙編語言的思想很容易了解。下面一段代碼說明函數名其實就是一個位址,代碼如下:
[cpp] view plain copy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- void abc()
- {
- printf("hello fun\n");
- }
- int main(int argc, char *argv[])
- {
- void (*d)();
- void (*p)();
- p = abc;
- abc();
- printf("%p\n",abc);
- printf("%p\n",&abc);//函數abc的位址0x40138c
- p();
- (*p)();
- d = ((unsigned int*)0x40138c); //其實就算d= 0x40138c這麼給指派也沒問題
- d();
- return 0;
- }
可見函數名就是一個位址,是以函數名abc與&abc沒有差別,是以p和*p也沒有差別。
b.我覺得函數指針最重要的是它的應用環境,如回調函數(其實就是利用函數指針,把函數當作參數進行傳遞)代碼如下,還有中斷處理函數(同理)詳細見<
ok6410學習筆記(16.按鍵中斷控制led)>中的 中斷注冊函數,request_irq。還有就是函數指針數組,第一次見到函數指針數組是在zigbee協定棧中。
回調函數原理代碼:
[cpp] view plain copy
- #include <stdio.h>
- typedef int(*FUNCTION)(int);
- int g(int n, FUNCTION f)
- {
- int i = 0;
- int ret = 0;
- for(i=1; i<=n; i++)
- {
- ret += i*f(i);
- }
- return ret;
- }
- int f1(int x)
- {
- return x + 1;
- }
- int f2(int x)
- {
- return 2*x - 1;
- }
- int f3(int x)
- {
- return -x;
- }
- int main()
- {
- printf("x * f1(x): %d\n", g(3, f1));
- printf("x * f2(x): %d\n", g(3, &f2));
- printf("x * f3(x): %d\n", g(3, f3));
- }
注意:可以使用函數名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] 函數指針 數組指針 也不好看 就放裡面了。