1. 程式人生 > >指標作為函式引數傳遞的問題

指標作為函式引數傳遞的問題

今天程式設計時遇到了一個問題,就是想寫一個函式,讓這個函式通過引數返回記憶體某一個緩衝區的首地址。在網上找了找,發現以下這段程式碼比較有代表性,
於是複製過來作為一個總結說明。
下面這段程式碼是正確的,這段程式碼通過函式申請一片記憶體區,並將申請到的記憶體首地址返回給呼叫函式:
void getptr(int **p,int num)
{
 *p=(int *)maccol(num*sizeof(int));
 return;
}
void main()
{    
int *ptr; 
int k; 
getptr(&ptr,10);
for(k=0;k<10;k++)
 { 
scanf("%d",&ptr[k]); 
}
}
下面這樣是不行的,原因在下面:
void getptr(int *p,int num) {
p=(int *)maccol(num*sizeof(int)); return;
}
void main()
{
int *ptr; int k; getptr(ptr,10);
for(k=0;k<10;k++)
{ scanf("%d",&ptr[k]); }
}
首先要說明的是int *ptr,這時候ptr指向一個不明確的地址,在這種情況下getptr(ptr,10);這一句傳一個地址給getptr裡面的p指標,這時候p指標獲得的是ptr指標
的一個副本,確切的說是ptr指向的物件的記憶體地址,所以這種方式在函式裡面可以用來傳一個指標指向的資料,這個資料是引用,但是指標還是副本,就是p與ptr兩個
所指的資料是一樣的,但是兩個在記憶體的地址是不一樣的. 而你這個程式要求的是給ptr動態分配記憶體,所以當分配了一塊的記憶體,並且這個記憶體的首地址賦給p,
這時候p指向的東西改變了,但沒有影響到ptr指標.ptr指標指向的還是他原來的資料,只是p指標的資料改變了. 所以這種情況引入一個指標的指標,讓p指標指
向ptr這個指標(而不是ptr的副本),這時候動態的分配的資料塊傳給*p,這個*p就是ptr,所以給ptr分配記憶體成功.
=====================================================================================

這幾天在學習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語句返回指向“棧記憶體”的指標,因為該記憶體在函式結束時自動消亡]

(注意:實際上沒有混淆,因為這裡tree是在“堆內”分配的記憶體,而非在“棧”上。)

所謂一份拷貝,就是在函式呼叫時,將引數入棧,我們對形參的任何修改都是修改到
棧上的個拷貝,並不影響我們的實際引數.

任何程式語言的引數傳遞實際上都是在做傳值呼叫.
所謂的傳指標,就是把指標指向者的地址(一個值)傳進函式.
也就是那個地址被壓棧.
然後我們再通過這個地址進行操作,因為實參和形參同樣都是一個地址的值.
所以改變形參指向者的狀態時,實參指標也能看到這種變化.

這裡區分一下靜態記憶體,棧記憶體和動態分配的記憶體(堆記憶體)的區別:
(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++程式設計指南》]

  1. <SPAN style="FONT-SIZE: 18px">#include<iostream>//指向指標的指標 
  2. usingnamespace std;  
  3. void GetMemory(char * &p,int num){  
  4.  p=(char *)malloc(sizeof(char)*num);  
  5. }  
  6. void main(void){  
  7. char *str=NULL;  
  8.  GetMemory(str,100);  
  9.  strcpy(str,"hello");  
  10.  cout<<str<<endl;  
  11.  free(str);  
  12. }</SPAN>  
  1. <SPAN style="FONT-SIZE: 18px">#include<iostream>//指向指標的指標 
  2. usingnamespace std;  
  3. void GetMemory(char * &p,int num){  
  4.  p=(char *)malloc(sizeof(char)*num);  
  5. }  
  6. void main(void){  
  7. char *str=NULL;  
  8.  GetMemory(str,100);  
  9.  strcpy(str,"hello");  
  10.  cout<<str<<endl;  
  11.  free(str);  
  12. }</SPAN>  
#include<iostream>//指向指標的指標
using namespace std;
void GetMemory(char * &p,int num){
 p=(char *)malloc(sizeof(char)*num);
}
void main(void){
 char *str=NULL;
 GetMemory(str,100);
 strcpy(str,"hello");
 cout<<str<<endl;
 free(str);
}

或者:
#include<iostream>

  1. <SPAN style="FONT-SIZE: 18px">usingnamespace std;  
  2. void GetMemory(char * *p,int num){  
  3.  *p=(char *)malloc(sizeof(char)*num);  
  4. }  
  5. void main(void){  
  6. char *str=NULL;  
  7.  GetMemory(&str,100);  
  8.  strcpy(str,"hello");  
  9.  cout<<str<<endl;  
  10.  free(str);  
  11. }</SPAN>  
  1. <SPAN style="FONT-SIZE: 18px">usingnamespace std;  
  2. void GetMemory(char * *p,int num){  
  3.  *p=(char *)malloc(sizeof(char)*num);  
  4. }  
  5. void main(void){  
  6. char *str=NULL;  
  7.  GetMemory(&str,100);  
  8.  strcpy(str,"hello");  
  9.  cout<<str<<endl;  
  10.  free(str);  
  11. }</SPAN>  
using namespace std;
void GetMemory(char * *p,int num){
 *p=(char *)malloc(sizeof(char)*num);
}
void main(void){
 char *str=NULL;
 GetMemory(&str,100);
 strcpy(str,"hello");
 cout<<str<<endl;
 free(str);
}

以上都是正確的下面的例子是錯誤的:

  1. <SPAN style="FONT-SIZE: 18px">#include<iostream>  
  2. usingnamespace std;  
  3. void GetMemory(char *p,int num){  
  4.  p=(char *)malloc(sizeof(char)*num);  
  5. }  
  6. void main(void){  
  7. char *str=NULL;  
  8.  GetMemory(str,100);  
  9.  strcpy(str,"hello");  
  10.  cout<<str<<endl;  
  11.  free(str);  
  12. }</SPAN>  
  1. <SPAN style="FONT-SIZE: 18px">#include<iostream>  
  2. usingnamespace std;  
  3. void GetMemory(char *p,int num){  
  4.  p=(char *)malloc(sizeof(char)*num);  
  5. }  
  6. void main(void){  
  7. char *str=NULL;  
  8.  GetMemory(str,100);  
  9.  strcpy(str,"hello");  
  10.  cout<<str<<endl;  
  11.  free(str);  
  12. }</SPAN>  
#include<iostream>
using namespace std;
void GetMemory(char *p,int num){
 p=(char *)malloc(sizeof(char)*num);
}
void main(void){
 char *str=NULL;
 GetMemory(str,100);
 strcpy(str,"hello");
 cout<<str<<endl;
 free(str);
}


試圖用指標申請動態記憶體,錯誤的原因上面已經給出了詳細的說明。總而言之,指標作為引數時,不能在函式體中改變指標的記憶體地址,要不然,實參的拷貝(壓入棧中)改變了,而實參沒有改變,造成記憶體洩露並且還達不到預期的效果。上面正確的2個例子都是通過另一種方法繞開了這個問題,改變指標的內容,例如:用了指向指標的指標,給指標的內容改變了,使其變為新分配記憶體的首地址,從而達到了效果。