Contents ...
udn網路城邦
C語言深度剖析
2015/12/06 13:00
瀏覽164
迴響0
推薦0
引用0
1.關於指針的數據類型:

指針變量 p 裏存儲的任何數據都將被當作地址來處理。可以這麽理解:一個基本的數據類型(包括結構體等自定義類型)加上“*”號就構成了一個指針類型的模子。這個模子的大小是一定的,與“*”號前面的數據類型無關。 “*”號前面的數據類型只是說明指針所指向的內存裏存儲的數據類型。編譯器會把存在指針變量中的任何數據當作地址來處理。


2.關於NULL:

註意NULL就是NULL,它被宏定義為 0。NUL是ASCII碼表的第一個字符,表示的是空字符,其ASCII碼值為 0。其值雖然都為0,但表示的意思完全不一樣。同樣,NULL和0表示的意思也完全不一樣。


3.關於數組:

當我們定義一個數組a時,編譯器根據指定的元素個數和元素的類型分配確定大小(元素類型大小*元素個數)的一塊內存,並把這塊內存的名字命名為a。a[0],a[1]等為a的元素,但並非元素的名字。數組的每一個元素都是沒有名字的。


4.關於int a[5]:

如:int a[5];sizeof(a[5])

函數求值是在運行的時候,而關鍵字 sizeof求值是關鍵字,它是在編譯的時候。雖然並不存在a[5]這個元素,但是這裏也並沒有去真正訪問 a[5],而是僅僅根據數組元素的類型來確定其值。所以這裏使用 a[5]並不會出錯。


5.關於&a[0]和&a的區別:

a[0]是一個元素,a是整個數組,雖然&a[0]和&a的值一樣,但其意義不一樣。前者是數組首元素的首地址,而後者是數組的首地址。


6.關於a和&a的區別:

a,&a的值是一樣的,但意思不一樣,a 是數組首元素的首地址,也就是 a[0]的首地址,&a是數組的首地址,a+1是數組下一元素的首地址,即a[1]的首地址,&a+1是下一個數組的首地址。

例: int main()

{

char a[5]={'A','B','C','D'};

char (*p3)[5] = &a;

char (*p4)[5] = a;

return 0;

}

由於a和&a的意義不同,則p3正確。p4這個定義的“=”號兩邊的數據類型就不一致了。左邊的類型是指向整個數組的指針,右邊的數據類型是指向單個字符的指針。


7.關於數組名作左值與右值:

如:x=y

左值:在這個上下文環境中,編譯器認為 x 的含義是x 所代表的地址。這個地址只有編譯器知道,在編譯的時候確定,編譯器在一個特定的區域保存這個地址,我們完全不必考慮這個地址保存在哪裏。

右值:在這個上下文環境中,編譯器認為 y 的含義是y 所代表的地址裏面的內容。這個內容是什麽,只有到運行時才知道。


a作為右值時其意義與&a[0]是一樣,代表的是數組首元素的首地址,而不是數組的首地址。

a不能作為左值,因為編譯器會認為數組名作為左值代表的意思是a的首元素的首地址,但是這個地址開始的一塊內存是一個總體,我們只能訪問數組的某個元素而無法把數組當一個總體進行訪問。所以我們可以把a[i]當左值,而無法把a當左值。


8.關於指針和數組的關系:

數組就是數組,指針就是指針,它們是完全不同的兩碼事!他們之間沒有任何關系。


9.以指針的形式訪問和以下標的形式訪問指針和數組:

以指針的形式訪問和以下標的形式訪問指針:

例:char *p = “abcdef”;

以指針的形式:*(p+4)。

以下標的形式:p[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。p[4]這個操作會被解析成:先取出p裏存儲的地址值,然後加上中括號中4個元素的偏移量,計算出新的地址,然後從新的地址中取出值。也就是說以下標的形式訪問在本質上與以指針的形式訪問沒有區別,只是寫法上不同罷了。


以指針的形式訪問和以下標的形式訪問數組:

例:char a[] = “123456”;

以指針的形式:*(a+4)。a這時候代表的是數組首元素的首地址。

以下標的形式:a[4]。同訪問指針。


由上可知:指針和數組根本就是兩個完全不一樣的東西。只是它們都可以“以指針形式”或“以下標形式”進行訪問。一個是完全的匿名訪問,一個是典型的具名+匿名訪問。


10.關於指針的操作:

對指針進行加1操作,得到的是下一個元素的地址。指針變量與一個整數相加減並不是用指針變量裏的地址直接加減這個整數。這個整數的單位不是byte而是元素的個數。

例: struct Test

{

int Num;

char *pcName;short sDate;

char cha[2];

short sBa[4];

}*p;

假設p = 0x100000,則:

p+0x1的值為:0x100000+sizof(Test)*0x1 = 0x100014。

(unsigned int*)p + 0x1的值為:0x100000+sizof(unsigned int)*0x1 = 0x100004。

例: int main()

{

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

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

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

printf("%x,%x",ptr1[-1],*ptr2);

return 0;

}

假設a = 0x00,則:

分析:

ptr1:&a + 1 = a + sizeof(a)*1 = 0x10,將&a+1的值強制轉換成int*類型,賦值給int* 類型的變量ptr,ptr1肯定指到數組a的下一個int類型數組了。ptr1[-1]被解析成*(ptr1-1),即ptr1往後退4個byte。所以其值為0x10 - 0x04 = 0x0c。

ptr2:(int)a+1的值是元素a[0]的第二個字節的地址。轉換成整型就是簡單的數學運算了。然後把這個地址強制轉換成int*類型的值賦給ptr2, 也就是說*ptr2的值應該為元素 a[0]的第二個字節開始的連續4個byte的內容,也就是說是a[0]的後三個字節和a[1]的第一個字節。這連續 4 個byte裏到底存了什麽東西呢?也就是說語素a[0],a[1]裏面的值到底怎麽存儲的。這就要看系統的大小端模式了。


11.關於指針數組和數組指針:

指針數組:首先它是一個數組,數組的元素都是指針,數組占多少個字節由數組本身決定。它是“儲存指針的數組”的簡稱。

數組指針:首先它是一個指針,它指向一個數組。在32位系統下永遠是占4個字節,至於它指向的數組占多少字節,不知道。它是“指向數組的指針”的簡稱。

例:A int *p1[10];

   B int (*p2)[10];

理解:這裏需要明白一個符號之間的優先級問題。“[]”的優先級比“*”要高。p1先與“[]”結合,構成一個數組的定義,數組名為 p1,int *修飾的是數組的內容,即數組的每個元素。那現在我們清楚,這是一個數組,其包含 10個指向 int類型數據的指針,即指針數組。至於 p2就更好理解了,在這裏“ () ”的優先級比“[]”高, “*”號和 p2構成一個指針的定義,指針變量名為 p2,int修飾的是數組的內容,即數組的每個元素。數組在這裏並沒有名字,是個匿名數組。那現在我們清楚 p2是一個指針,它指向一個包含10個int類型數據的數組,即數組指針。


12.關於指針函數和函數指針:

指針函數:首先它是一個函數,是指帶指針的函數,即本質是一個函數。返回類型是某一類型的指針。

其定義格式如下所示:

返回類型標識符 *返回名稱(形式參數表){}

函數指針:首先它是一個指針,是指向函數的指針變量,因而“函數指針”本身首先應是指針變量,只不過該指針變量指向函數。

其定義格式如下所示:

數據類型標誌符 (*指針變量名)(參數){}

分析:同樣可以用優先級來分析。


13.關於二維數組:

例:char a[3][4];

實際上內存不是表狀的,而是線性的。平時我們說內存地址為 0x0000FF00也是指從內存零地址開始偏移0x0000FF00個byte。編譯器總是將二維數組看成是一個一維數組, 而一維數組的每一個元素又都是一個數組。

a[i]的首地址為:&a[0]+ i*sizof(char)*4

a[i][j]的首地址為:&a[i]+j*sizof(char)

a[i][j]元素的首地址為:a+ i*sizof(char)*4+ j*sizof(char),以指針的形式表示:*(*(a+i)+j)。(因為a+i為第i個數組的首地址,則*(a+i)為第i個數組的首元素地址)


例: #include

int main(int argc,char * argv[])

{

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

int *p;

p=a [0];

printf("%d",p[0]);

}

問打印出來的結果是多少?

分析:答案為1,不是0!花括號裏面嵌套的是小括號,而不是花括號!這裏是花括號裏面嵌套了逗號表達式!其實這個賦值就相當於 int a[3][2]={ 1, 3, 5};而二維數組的初始化為:int a[3][2] = {{1,2}, {3,4}, {5,6}};


14.關於數組內存計算:

例: int main()

{

int a[5][5];

int (*p)[4];

p=a;

printf("a_ptr=%#p,p_ptr=%#p\n",&a[4][2],&p[4][2]);

printf("%p,%d\n",&p[4][2] - &a[4][2],&p[4][2] - &a[4][2]);

return 0;

}

分析:&p[4][2] - &a[4][2]的值為-4.&a[4][2]表示的是&a[0][0]+4*5*sizeof(int) + 2*sizeof(int)。&p[4][2]表示的是&a[0][0]+4*4*sizeof(int)+2* sizeof(int)。


15.關於多級指針:

與一級指針不同的是,一級指針保存的是數據的地址,二級指針保存的是一級指針的地址。


16.關於數組作參數:

例: char b[10] = “abcdefg”;

fun(b[10]);

分析:這裏數組越界了,這個b[10]並不存在。但在編譯階段,編譯器並不會真正計算b[10]的地址並取值,所以在編譯的時候編譯器並不認為這樣有錯誤。


無法向函數傳遞一個數組,C語言中,當一維數組作為函數參數的時候,編譯器總是把它解析成一個指向其首元素首地址的指針。在 C 語言中,所有非數組形式的數據實參均以傳值形式(對實參做一份拷貝並傳遞給被調用的函數,函數不能修改作為實參的實際變量的值,而只能修改傳遞給它的那份拷貝)調用。同樣的,函數的返回值也不能是一個數組,而只能是指針。需要明確一個概念::函數本身是沒有類型的,只有函數的返回值才有類型。


17.關於指針作參數:

例: void GetMemory(char * p, int num)

{

p = (char *)malloc(num*sizeof(char));

}

int main()

{

char *str = NULL;

GetMemory(str,10);

strcpy(str,”hello”);

free(str) ;//free並沒有起作用,內存泄漏

return 0;

}

分析:無法把指針變量本身傳遞給一個函數,在運行 strcpy(str,”hello”)語句的時候發生錯誤。 這時候觀察 str的值, 發現仍然為NULL。也就是說 str本身並沒有改變,我們malloc的內存的地址並沒有賦給str,而是賦給了_str。而這個_str是編譯器自動分配和回收的,我們根本就無法使用。

那怎麽辦? 兩個辦法:

第一:用 return。

char * GetMemory(char * p, int num)

{

p = (char *)malloc(num*sizeof(char));

return p;

}

int main()

{

char *str = NULL;

str = GetMemory(str,10);

strcpy(str,”hello”);

free(str) ;

return 0;

}

第二:用二級指針。

void GetMemory(char ** p, int num)

{

*p = (char *)malloc(num*sizeof(char));

return p;

}

int main()

{

char *str = NULL;

GetMemory(&str,10);

strcpy(str,”hello”);

free(str) ;

return 0;

}

這樣的話傳遞過去的是 str的地址,是一個值。在函數內部,用鑰匙(“*”)來開鎖:*(&str),其值就是str。所以malloc分配的內存地址是真正賦值給了str本身。


18.關於二維數組和二維指針作函數參數:

例: void fun(char a[3][4]);

分析:C 語言中,當一維數組作為函數參數的時候,編譯器總是把它解析成一個指向其首元素首地址的指針。也就是說我們可以把這個函數聲明改寫為:

void fun(char (*p)[4]);

同樣,作為參數時,一維數組“[]”號內的數字完全可以省略:void fun(char a[][4]);不過第二維的維數卻不可省略。

註意:如果把上面提到的聲明 void fun (char (*p)[4])中的括號去掉之後, 聲明 “void fun(char *p[4])”可以改寫成:void fun(char **p);


數組參數 等效的指針參數

數組的數組:char a[3][4] 數組的指針:char (*p)[10]

指針數組: char *a[5] 指針的指針:char **p

這裏需要註意的是:C語言中,當一維數組作為函數參數的時候,編譯器總是把它解析成一個指向其首元素首地址的指針。這條規則並不是遞歸的,也就是說只有一維數組才是如此,當數組超過一維時,將第一維改寫為指向數組首元素首地址的指針之後,後面的維再也不可改寫。


19.關於函數指針:

例: char* (*fun1)(char* p1,char* p2); ------A

char* *fun2(char* p1,char* p2); ------B

char* fun3(char* p1,char* p2); ------C

分析:A既是函數指針也是指針函數,返回值類型都為為char*。B是指針函數,返回值類型為char**,是個二級指針。C是指針函數,返回值類型為char*。


函數指針的使用例子:

char * fun(char * p1,char * p2)

{

......

return p1;

}

int main()

{

char* (*pf)(char * p1,char * p2);

pf = &fun;

(*pf) ("aa","bb");

return 0;

}

分析:給函數指針賦值時,可以用&fun或直接用函數名fun。這是因為函數名被編譯之後其實就是一個地址,所以這裏兩種用法沒有本質的差別。


20.關於(*(void(*) ())0)():

分析:

第一步:void(*) (),可以明白這是一個函數指針類型。這個函數沒有參數,沒有返回值。

第二步:(void(*) ())0,這是將 0強制轉換為函數指針類型,0是一個地址,也就是說一個函數存在首地址為0的一段區域內。

第三步:(*(void(*) ())0),這是取0地址開始的一段內存裏面的內容,其內容就是保存在首地址為0的一段區域內的函數。

第四步:(*(void(*) ())0)(),這是函數調用。


21.關於函數指針數組:

例:char* (*pf[3])(char * p);

分析:這是定義一個函數指針數組。它是一個數組,數組名為 pf,數組內存儲了3 個指向函數的指針。這些指針指向一些返回值類型為指向字符的指針、參數為一個指向字符的指針的函數。


函數指針數組的使用:

#include

#include

char * fun1(char * p){}

char * fun2(char * p){}

char * fun3(char * p){}

int main()

{

char * (*pf[3])(char * p);

pf[0] = fun1; // 可以直接用函數名

pf[1] = &fun2; // 可以用函數名加上取地址符

pf[2] = &fun3;

pf[0]("fun1");

pf[0]("fun2");

pf[0]("fun3");

return 0;

}


22.關於函數指針數組的指針:

例:char* (*(*pf)[3])(char * p);

分析:pf這個指針指向一個包含了3個元素的數組;這個數字裏面存的是指向函數的指針;這些指針指向一些返回值類型為指向字符的指針、參數為一個指向字符的指針的函數。


函數指針數組的指針的使用:

#include

#include

char * fun1(char * p){}

char * fun2(char * p){}

char * fun3(char * p){}

int main()

{

char * (*a[3])(char * p);

char * (*(*pf)[3])(char * p);

pf = &a;

a[0] = fun1;

a[1] = &fun2;

a[2] = &fun3;

pf[0][0]("fun1");

pf[0][1]("fun2");pf[0][2]("fun3");

return 0;

}

限會員,要發表迴響,請先登入