1. 程式人生 > >C語言深度解剖讀書筆記(4.指標的故事)

C語言深度解剖讀書筆記(4.指標的故事)

指標這一節是本書中最難的一節,尤其是二級指標和二維陣列直接的關係。

本節知識點:

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的,陣列的都是一樣的。 這個有一個例子,程式碼如下:
#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;
	
}
5.訪問指標和訪問陣列的兩種方式:     分別是以下標方式訪問和以指標的方式訪問,我覺得沒有任何區別,*(p+4)和p[4]是一樣的 ,其實都可以理解成指標運算。如果非要說出區別,我覺得指標的方式會快些,但是在當前的硬體和編譯器角度看,不會太明顯。同樣下標的方式可讀性可能會高些。 6.切記陣列不是指標:
    陣列是陣列,指標是指標,根本就是兩個完全不一樣的東西。當然要是在巨集觀的記憶體角度看,那一段相同型別的連續空間,可以說的上是陣列。但是你可以嘗試下,定義一個指標,在其他地方把他宣告成陣列,看看編譯器會不會把兩者混為一談,反過來也不會。     但是為什麼我們會經常弄混呢?第一,我們常常利用指標的方式去訪問陣列。第二,陣列作為函式引數的時候,編譯器會把它退化成為指標,因為函式的引數是拷貝,如果是一個很大的陣列,拷貝是很浪費記憶體的,所以陣列會被退化成指標(這裡一定要理解好,退化的是陣列成員的型別指標,不一定是陣列指標的哈)。 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]  函式指標 陣列指標  也不好看 就放裡面了。