在人人網上看到了一篇C指針的總結,講一些基本概念,寫的不錯,原地址:http://blog.renren.com/blog/226041764/873143492
char *p, **p, ***p;
char p[],p[][],p[][][];
char *p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];
void (*pFun)(int i);
大神們看到這些東西腦袋裏一定像藍天白雲一樣清晰明瞭。。。
(1)關於指針與數組的存儲
a、指針和數組在內存中的存儲形式
數組p[N]創建時,對應着內存中一個數組空間的分配,其地址和容量在數組生命週期內一般不可改變。數組名p本身是一個常量,即分配數組空間的地址值,這個值在編譯時會替換成一個常數,在運行時沒有任何內存空間來存儲這個值,它和數組長度一起存在於代碼中(應該是符號表中),在鏈接時已經制定好了;而指針*p創建時,對應內存中這個指針變量的空間分配,至於這個空間內填什麼值即這個指針變量的值是多少,要看它在程序中被如何初始化,這也決定了指針指向哪一塊內存地址。
圖畫的真醜,不過全文就一張圖,湊合看吧
b、指針和數組的賦值與初始化
根據上文,一般情況下,數組的地址不能修改,內容可以修改;而指針的內容可以修改,指針指向的內容也可以修改,但這之前要爲指針初始化。
如:
int p[5];
p=p+1; 是不允許的
而p[0]=1; 是可以的;
//
int *p;
p=p+1; 是允許的
p[0]=1; 是不允許的,因爲指針沒有初始化;
//
int i;
int *p=&i;
p[0]=1; 是允許的;
對於字符指針還有比較特殊的情況。
如:
char * p="abc";
p[0]='d'; 是不允許的
爲什麼初始化了的字符指針不能改變其指向的內容呢?這是因爲p指向的是“常量”字符串,字符串"abc"實際是存儲在程序的靜態存儲區的,因此內容不能改變。這裏常量字符串的地址確定在先,將指針指向其在後。
而
char p[]="abc";
p[0]='d'; 是允許的
這是因爲,這個初始化實際上是把常量直接賦值給數組,即寫到爲數組分配的內存空間。這裏數組內存分配在先,賦值在後。
(2)關於一些表達式的含義
char *p, **p, ***p;
char p[],p[][],p[][][];
char *p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];
能清晰地知道以上表達式的含義嗎?(知道的去死!)
第一組:char *p, **p, ***p;
分別爲char指針;char*指針,即指向char*類型數據地址的指針;char**指針,即指向char**類型數據的指針;他們都是佔4字節空間的指針。
如:
char c='a';
char *p=&c;
char **p1=&p;
char ***p2=&p1;
cout<<***p2<<endl;
第二組:char p[],p[][],p[][][];
分別爲一維,二維和三維char型數組,即數組,數組的數組,<數組的數組>的數組。可以如下的方式進行初始化:
char pp[3]="ab";
char pp1[3][3]={"ab"};
char pp2[3][3][3]={{"ab"}};
現在我們嘗試使用第二組三個數組名對應爲第一組三個指針賦值,直接賦值的結果如下:
p=pp; //正確
p1=pp1; //錯誤
p2=pp2; //錯誤
爲什麼p1和p2的賦值會出錯呢?原因是數組名爲給指針賦值的規則不是遞歸的,即數組的數組可以爲數組的指針賦值,而不可以爲指針的指針賦值。這裏先了解到這個抽象的規則,下面講完第三組表達式,等我們知道數組的指針和指針的數組如何書寫後再對這一問題舉例說明。
第三組:char *p[],*p[][],**p[],**p[][],*(*p)[],(**p)[],(**p)[][];
這一類表達式的解析方法如下:
首先把整個表達式分爲三部分,
數據類型和星號部分+p或括號內內容部分+中括號部分
如:char *(*p)[]分爲char* ,(*p) 和 []
“char*”表示最內層存儲的數據類型“(*p)”表示最外層指針“[]”表示中間層數組(維數=中括號數目),因此上式表示一個一維數組的指針p,數組中的元素的數據類型是指針char*。同理,char (**p)[][]表示,一個二維數組的指針的指針,數組元素的數據類型是char。這裏如果表達式中間沒有括號(如**p[]),則實際上是一個數組,如果最右沒有中括號(如**p),則實際上是一個指針。下面通過賦值表達式來理解這些表達式的含義:
char c='a';
char *pc=&c;
char *p[3],*p1[3][3],**p2[3],**p3[3][3],*(*p4)[3],(**p5)[3],(**p6)[3][3],(*(*p7))[3];
p[1]=pc;
p1[0][0]=pc;
p2[0]=&pc;
p3[0][0]=&pc;
(*p4)[0]=pc;
(**p5)[0]=c;
(**p6)[0][0]=c;
(**p7)[0]=c;
注意,(*(*p7))[3]和(**p5)[3]是等價的。
這裏再繼續上一小節講一下數組名給指針賦值的問題。
事實上可以對等賦值的數組和指針關係如下(——>表示“賦值給”):
數組——>指針 : p[]——>*p
指針的數組——>指針的指針 : *p[]——>**p
指針的指針的數組的——>指針的指針的指針 : **p[]——>***p
。。。。。。
或
數組的數組——>數組的指針 : p[][]——>(*p)[]
數組的數組的數組的——>數組的數組的指針 : p[][][]——>(*p)[][]
總之,最外層的數組可以轉換指針,往內層不遞歸。
(3)關於上述表達式的長度
求一個表達式的“長度”,首先分清表達式實際表示的是一個數組還是一個指針;如果是指針,則長度爲4byte;如果爲數組則要計算實際存儲的總元素個數和元素的數據類型。另外要注意要求的是數組元素個數還是數組總字節數;
如:
*(*p)[3][3]
由上文可知上式表示一個指針,因此長度爲4byte;而
**p3[3][3]
表示一個二維數組,數組元素類型爲指針的指針,因此長度爲3*3*4=36;
注意,標準C中sizeof函數求得的是總字節數而非數組長度。
(4)關於函數的指針返回值和指針參數
指針作爲返回值要注意的地方是不要返回局部數據的指針。
如:
char * fun(void)
{
char i='a';
return (&i);
}
調用函數fun得不到值'a',原因是函數返回後,局部數據(在棧中)被析構,數據內存被回收,指針指向的數據沒有意義;
可以改爲:
char * fun(void)
{
char i='a';
char *p=(char *)malloc(5);
If(p!=NULL) {p[0]=i, p[1]='\0';}
return (p);
}
這裏使用malloc分配了內存(在堆中)在函數返回後依然有效。
這裏還沒完,因爲有一天我使用了下面的代碼:
char * fun(void)
{
char *p="abc";
return (p);
}
發現雖然p定義爲局部變量,但返回也是正確的。還記得上面講到的常量字符串存儲位置嗎?指針p實際指向了靜態數據區,這裏的char *p相當於const char *p,這樣的數據在函數返回後是不會被立刻回收掉的。
指針作爲參數主要用於修改指針指向的數據內容,但修改指針的值無效,
如
char * fun(char *p)
{
char i='a';
p=(char *)malloc(5);
p[0]=i;
return p;
}
因爲傳遞的是一個指針副本(形參指針和實參指針的值相同,但不是同一個指針),不會影響調用方的實參值。(詭異的vs2012貌似可以通過形參將實參的值改掉!不過還是不要冒這個險爲好了)。
(5)關於const修飾符
const修飾符用於指針時也非常糾結。
首先要分清const char *p和char* const p。
const char *p是指向const對象的指針,即對象是隻讀的,而指針不是。使用const對象的指針要注意兩點:
一是不能將其賦值給非const對象的指針,
如:
const char* p;
char *p1=p; //不允許的
當然,直接使用非const指針指向const對象也是不合法的,
如:
const char c;
char *p1=&c;//不允許的,
這是要避免通過上述p1改變const對象的值。
二是可以將非const對象的地址賦值給指向const對象的指針,但是試圖使用這個指針改變變量的值是非法的,
如:
char c='a';
const char* p=&c;//允許的
*p='b';//不允許的
char* const p是const指針,即指向對象可編輯,但指針本身不可修改,這一點類似於數組。
如:
char c='a';
char* const p=&c;
*p='b';//允許的
p++;//不允許的
區分兩者的方法是,看const是否靠近指針名,如果是則爲const指針,否則爲const對象。這個助記方法的前提是char要和*號靠在一起,因爲const char* p=char const *p。
另外,還有const char* const p,自然是指向const對象的const指針了。
(6)關於指針函數
首先注意指針函數和函數指針的區別,前者是指“返回指針的函數”,這在上文中有提到,而後者是指“指向函數的指針”。
函數指針的定義方法爲,將“函數名”替換爲“(*函數指針名)”,
如:
指向一個聲明爲void fun(int a)的函數指針可以定義爲 void (*pFun)(int a)或void (*pFun)(int ),注意這裏函數指針pFun只能指向和fun有相同返回類型(void)和參數類型(int)的一類函數,另外定義中()也不是擺設,去掉()會被看做是返回值爲void*類型的函數聲明。舉個例子:
void fun(int a)
{
cout<<a<<endl;
}
int main()
{
void (*pFun)(int);
pFun=&fun; //(1)
*(pFun)(1); //(2)
}
事實上,上式中的(1)(2)行做如下幾種替換也是正確的:
a、pFun=fun;
pFun(1);
b、pFun=&fun;
pFun(1);
c、pFun=fun;
*(pFun)(1);
如果有什麼疑問的話,可以接着嘗試用如下方式直接調用函數fun:
(*fun)(1);
運行的結果也是正確的!這要怎麼解釋呢?
其實,fun不僅僅作爲函數名,它同pFun一樣也是一個函數指針,只不過是函數指針常量。爲了書寫方便,c語言開發者允許將函數指針調用直接寫成類似fun()的形式,同樣函數指針變量賦值也可以寫成類似pFun=&fun的形式。值得注意的是,函數聲明的格式還是比較嚴格的,如:
void fun(int ); //不能寫成void (*fun)(int )。
同樣,
void (*pFun)(int ); //不能寫成void pFun(int )。
爲了方便,我們還可以定義函數指針類型,針對上述例子的定義方法如下:
typedef void (*PFUN)(int);
這樣一來我們就可以用
PFUN pFun;
來聲明一個函數指針了。
有了函數指針之後,函數的參數也可以設爲某一類函數類型。
如:
typedef void (*PFUN)(int);
void fun(int a)
{
cout<<a<<endl;
}
void topfun(PFUN f1, int a)
{
f1(a);
}
int main()
{
topfun(fun,1);
return 1;
}