關於指標作為引數傳遞的一些探討
值傳遞, 指標傳遞?
這幾天在學習C過程中,在使用指標作為函式引數傳遞的時候出現了問題,根本不知道從何得解:原始碼如下:
createNode(BinNode *tree,char *p)
{
tree = (BinNode *) malloc(sizeof(BinNode));
tree->data = *p;
}
該程式碼段的意圖是通過一個函式建立一個二叉樹的節點,然而在,呼叫該函式後,試圖訪問該節點結構體的成員時候,卻發生了記憶體訪問錯誤,到底問題出在哪兒呢?
一直不明白指標作為函式引數傳值的機制,翻開林銳的《高質量C/C++程式設計指南》,找到了答案。
[如果函式的引數是一個指標,不要指望用該指標去申請動態記憶體]
原來問題出在C編譯器原理上:編譯器總是要為函式的每個引數製作臨時副本,指標引數tree的副本是 _tree,編譯器使 _tree = tree。如果函式體內的程式修改了_tree的內容,就導致引數tree的內容作相應的修改。這就是指標可以用作輸出引數的原因。
即上面的函式程式碼經過編譯後成為:
createNode(BinNode *tree,char *p)
{
BinNode *_tree;
_tree = tree;
_tree = (BinNode *) malloc(sizeof(BinNode));
_tree->data = *p;
}
如果沒有
_tree = (BinNode *) malloc(sizeof(BinNode));
這個語句,在函式體內修改了_tree的內容,將會導致引數tree的內容作相應的修改,因為它們指向相同的記憶體地址。
_tree = (BinNode *) malloc(sizeof(BinNode));
這個句,系統重新分配記憶體給_tree指標,_tree指標指向了系統分配的新地址,函式體內修改的只是_tree的內容,對原tree所指的地址的內容沒有任何影響。因此,函式的引數是一個指標時,不要在函式體內部改變指標所指的地址,那樣毫無作用,需要修改的只能是指標所指向的內容。即應當把指標當作常量。
如果非要使用函式指標來申請記憶體空間,那麼需要使用指向指標的指標
createNode(BinNode **tree,char *p)
{
*tree = (BinNode *) malloc(sizeof(BinNode));
}
上面的是林銳的說法,目前來說不知道怎麼去理解,不過可以有另外的方案,通過函式返回值傳遞動態記憶體:
BinNode *createNode()
{
BinNode *tree;
tree = (BinNode *) malloc(sizeof(BinNode));
return tree;
}用函式返回值來傳遞動態記憶體,可行
這個倒還說得過去,因為函式返回的是一個地址的值,該地址就是申請的記憶體塊首地址。但是,這個容易和另外的一個忠告相混繞
[不要用return語句返回指向“棧記憶體”的指標,因為該記憶體在函式結束時自動消亡]
這裡區分一下靜態記憶體,棧記憶體和動態分配的記憶體(堆記憶體)的區別:
(1) 從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static變數。
(2) 在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
(3) 從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc或new申請任意多少的記憶體,程式設計師自己負責在何時用free或delete釋放記憶體。動態記憶體的生存期由我們決定,使用非常靈活,但問題也最多。
因此,試圖返回一個棧上分配的記憶體將會引發未知錯誤
char *GetString(void)
{
char p[] = "hello world";
return p; // 編譯器將提出警告
}失敗,不可行
p是在棧上分配的記憶體,函式結束後將會自動釋放,p指向的記憶體區域內容不是"hello world",而是未知的內容。
如果是返回靜態儲存的記憶體呢:
char *GetString(void)
{
char *p = "hello world";
return p;
}
這裡“hello world”是常量字串,位於靜態儲存區,它在程式生命期內恆定不變。無論什麼時候呼叫GetString,它返回的始終是同一個“只讀”的記憶體塊。
[參考:林銳《高質量C/C++程式設計指南》]
補充:
void pointer(int *p)
{
int a = 11;
printf("\nthe p is %p , addr is %d, *p is %d",p , &p, *p);
*p =11;
printf("\nthe p is %p , addr is %d, *p is %d",p , &p, *p);
p = &a;
printf("\nthe p is %p , addr is %d, *p is %d",p , &p, *p);
}
int main()
{
int b =22;
int *p = &b;
printf("\nthe p is %p , addr is %d, *p is %d",p , &p, *p);
pointer(p);
printf("\nthe p is %p , addr is %d, *p is %d",p , &p, *p);
}
the p is 0xbfd46498 , addr is -1076599652, *p is 22
the p is 0xbfd46498 , addr is -1076599680, *p is 22
the p is 0xbfd46498 , addr is -1076599680, *p is 11
the p is 0xbfd4646c , addr is -1076599680, *p is 11
the p is 0xbfd46498 , addr is -1076599652, *p is 11
1、例子中,指標p的拷貝傳入了方法中(其地址變了,說明是另一變數;值和指向的記憶體塊資料沒變)
2、將p的拷貝視作p1,p1改變了其所指向的記憶體塊的值為11
3、p1的值改變為a的地址,即p1指向a,此時p1與p分別指向不同的記憶體塊了,不會互相影響
4、方法結束,p地址和值沒變(改變的僅僅是p的拷貝p1),但是p所指向的記憶體塊資料被p1所改變了,故*p為11
總結:傳入的指標僅僅是一個拷貝,方法不會改變原指標的地址、值,但是可能會改變原指標所指向記憶體塊的資料。
再補充:void main()
{
char *p=NULL;
myMalloc(p); //這裡的p實際還是NULL,p的值沒有改變,為什麼?
if(p) free(p);
}
void myMalloc(char *s) //我想在函式中分配記憶體,再返回
{
s=(char *) malloc(100);
}
myMalloc(p)的執行過程:
分配一個臨時變數char *s,s的值等於p,也就是NULL,但是s佔用的是與p不同的記憶體空間。此後函式的執行與p一點關係都沒有了!只是用p的值來初始化s。
然後s=(char *) malloc(100),把s的值賦成malloc的地址,對p的值沒有任何影響。p的值還是NULL。
注意指標變數只是一個特殊的變數,實際上它存的是整數值,但是它是記憶體中的某個地址。通過它可以訪問這個地址。
程式2:void myMalloc(char **s)
{
*s=(char *) malloc(100);
}
void main()
{
char *p=NULL;
myMalloc(&p); //這裡的p可以得到正確的值了
if(p) free(p);
}
程式2是正確的,為什麼呢?看一個執行過程就知道了:
myMalloc(&p);將p的地址傳入函式,假設儲存p變數的地址是0x5555,則0x5555這個地址存的是指標變數p的值,也就是Ox5555指向p。
呼叫的時候同樣分配一個臨時變數char **s,此時s 的值是&p的值也就是0x5555,但是s所佔的空間是另外的空間,只不過它所指向的值是一個地址:Ox5555。
*s=(char *) malloc(100);這一句話的意思是將s所指向的值,也就是0x5555這個位置上的變數的值賦為(char *) malloc(100),而0x5555這個位置上存的是恰好是指標變數p,這樣p的值就變成了(char *) malloc(100)的值。即p的值是新分配的這塊記憶體的起始地址。
這個問題理解起來有點繞,關鍵是理解變數作函式形參呼叫的時候都是要分配一個副本,不管是傳值還是傳址。傳入後就和形參沒有關係了,它不會改變形參的值。myMalloc(p)不會改變p的值,p的值當然是 NULL,它只能改變p所指向的記憶體地址的值。但是myMalloc(&p)為什麼就可以了,它不會改變(&p)的值也不可能改變,但是它可以改變(&p)所指向記憶體地址的值,即p的值