C語言深度解剖讀書筆記(4.指標的故事)
指標這一節是本書中最難的一節,尤其是二級指標和二維陣列直接的關係。
本節知識點:
1.指標基礎,一張圖說明什麼是指標:2.跨過指標,直接去訪問一塊記憶體: 只要你能保證這個地址是有效的 ,就可以這樣去訪問一個地址的記憶體*((unsigned int *)(0x0022ff4c))=10; 但是前提是 0x0022ff4c是有效地址。對於不同的編譯器這樣的用法還不一樣,一些嚴格的編譯器,當你定義一個指標,把這個指標賦值為一個這樣的地址的時候,當檢測到地址無效,編譯的時候就會報錯!!!如果一些不太嚴格的編譯器,不管地址有效無效都會編譯通過,但是對於無效地址,當你訪問這塊地址的時候,程式就會執行停止! 3.a &a &a[0]三者的區別:
第二個意義是 陣列名 sizeof(a) 為整體陣列有多少個位元組 &a :這個是陣列的地址 。跟a的區別就是,a是一個 int* 的指標(在第一種意義的時候) ,而&a是一個 int (*p)[5]型別的陣列指標,指標運算的結果不一樣。(此處的int* 僅僅是為了舉例子,具體應該視情況而定) 4.指標運算(本節最重要的知識點,但並不是最難的,所以的問題都來源於這兒):
5.訪問指標和訪問陣列的兩種方式: 分別是以下標方式訪問和以指標的方式訪問,我覺得沒有任何區別,*(p+4)和p[4]是一樣的 ,其實都可以理解成指標運算。如果非要說出區別,我覺得指標的方式會快些,但是在當前的硬體和編譯器角度看,不會太明顯。同樣下標的方式可讀性可能會高些。 6.切記陣列不是指標:#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { /* int a[20]={1,2,4}; printf("%d\n",sizeof(a)); printf("%p\n",a); printf("%p\n",&a); printf("%p\n",&a[0]); */ /* int a[5]={1,2,3,4,5}; int (*p)[5]=&a; printf("%d\n",*((int *)(p+1)-1)); */ 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; }
陣列是陣列,指標是指標,根本就是兩個完全不一樣的東西。當然要是在巨集觀的記憶體角度看,那一段相同型別的連續空間,可以說的上是陣列。但是你可以嘗試下,定義一個指標,在其他地方把他宣告成陣列,看看編譯器會不會把兩者混為一談,反過來也不會。 但是為什麼我們會經常弄混呢?第一,我們常常利用指標的方式去訪問陣列。第二,陣列作為函式引數的時候,編譯器會把它退化成為指標,因為函式的引數是拷貝,如果是一個很大的陣列,拷貝是很浪費記憶體的,所以陣列會被退化成指標(這裡一定要理解好,退化的是陣列成員的型別指標,不一定是陣列指標的哈)。 7.弄清陣列的型別: 陣列型別是由陣列元素型別和陣列長度兩個因素決定的,這一點在陣列中體現的不明顯,在陣列指標的使用中體現的很好。
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函式的使用,安全性檢測很重要):
#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,程式碼如下:
#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也行?程式碼如下:
#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]有的時候也表示這個意思),這些值都是相等,但是他們型別不相同,行為也就不相同,意義也不相同。分析他們一定要先搞清,他們分別代表什麼。 下面是一個,二維陣列中指標運算的練習(指標運算的規則不變,型別決定行為):
#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.方式四:其實二維陣列在記憶體中也是連續的,這麼看也是一個一維陣列,所以就可以使用這個方式,利用陣列成員型別的指標。
int *q;
q = (int *)a;
printf("%d\n",*(q+6));
e.方式五:二維陣列中是由多個一維陣列組成的,所以就可以利用陣列指標來訪問二維陣列。
int (*p)[3];
p = a;
printf("%d\n",*(*(p+1)+1));
給一個整體的程式程式碼:
#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));
}
總結:對於二位陣列int a[3][3] 要想定義一個指標指向這個二維陣列的陣列元素(即a[0]等一維陣列),就要使用陣列指標,這個陣列指標要跟陣列型別相同。a[0]等陣列型別是元素型別是int,長度是3,所以陣列指標就要定義成int (*p)[3]。後面的這個維度一定要匹配上,不然的話型別是不相同的。
這裡有一個程式,要記得在c編譯器中編譯,這個程式能看出型別相同的重要性:
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%d\n", &p[4][2] - &a[4][2]);
}
12.二級指標:
a.因為指標同樣存在傳值呼叫和傳址呼叫,並且還有指標陣列這個東西的存在,所以二級指標還是有它的存在價值的。
b.常使用二級指標的地方:
(1)函式中想要改變指標指向的情況,其實也就是函式中指標的傳址呼叫,如:重置動態空間大小,程式碼如下:
#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*指標的地址。
#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* ;指標陣列會退化成二級指標;二維陣列會退化成一維陣列指標;三維陣列會退化成二維陣列指標(三維陣列的這個是我猜得,如果說錯了,希望大家幫我指出來,謝謝)。如圖:
二維陣列作為實參的例子:
#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.對於函式名來說,它是函式的入口,其實函式的入口就是一個地址,這個函式名也就是這個地址。這一點用匯編語言的思想很容易理解。下面一段程式碼說明函式名其實就是一個地址,程式碼如下:
#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協議棧中。
回撥函式原理程式碼:
#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] 函式指標 陣列指標 也不好看 就放裡面了。