函式呼叫參數傳遞

在C語言中,寫一個大型的程式一定不可能只單單的使用一個main()的函式就把整個程式寫完,最重要的就是將大型的程式切成一個模組一個模組,在將各個小問題的模組一個一個解決,這樣的程式看起來又有架構,可讀性又高,但是通常初學者最常問的問題就是陣列傳到副程式,或是指標傳到副程式等等的問題,如此就來分析一下,所有可以傳遞到副程式的寫法和觀念。

※ 函式的基本概念

* 當寫一個函式的時候都需要一個「宣告」(prototype)
* 函式分成三個部份「函式名稱」「傳入值」「回傳值」

* 函式傳入值有「傳址」(pass an address by value)「傳值」(pass by value)
* 呼叫函式方傳入值叫「實際參數」,而副函式接受值叫「形式參數」
* (重要) 傳入的變數是什麼型別,副函式就要以什麼型別去接受
* (超重要) 不可回傳函式中建立的指標,這邊講清楚,傳「指標」代表傳這個指標的內容值,就是所謂的「記憶體位址」

以上是最基本函式的觀念,以上這些名詞請一定要記熟,而且最重要的就是「呼叫怎麼傳入,函式怎麼接受」,而不可以回傳在函式中所建立的指標,若回傳malloc得到的位址的話例外。

※ 記憶體配置邏輯圖

* 一個程式運作的時候有三個記憶體儲存區分別為Global、Stack、Heap
* Global 是拿來存放「與程式共存亡」的變數,如全域變數、字串常數、static的變數等
* Stack 是拿來存放「離開block後會消失」的變數,如在函式中(包含main())的變數
* Heap 是一塊空間,讓你在程式執行時,建立變數(malloc)所存放的資料空間
* 如果不了解的話,請參考本Blog中的「變數生命週期」
* 而我們把變數傳給函式的時候,都是把這些資料存放在Stack中,Stack中的變數有個特性,當你離開這個block的時候或是離開函式的時候,Stack中的值,就會變的無效值 (非常重要)
* 所以函式可以回傳值,但是不可以回傳指向這個Stack某塊記憶體的位址,因為解參考(dereference)的時候會錯誤,因為資料早就不存在了

※ 解析型別

有了以上的概念之後,開始進入我們如何傳遞一個變數或是陣列或是指標給函式,但是還要做一些「型別」認知的訓練。

Example:

int x; //x is int
(x型別為「int」)
int* x; //x is pointer, point to int
(x型別為指向int型態的指標「int*」)
int x[10]; //x is an array of 10 ints
(x型別為指向int型態的常數指標「int* const」)
int x[10][10]; //x is an array of 10 arrays of 10 ints
(x型別為指向具有十個元素,每個元素為int,的指標「int (*)[10]」)
int x[10][10][10]; //x is an array of 10 arrays of 10 arrays of 10 ints
(x型別為指向具有十個元素,每個元素為十個元素,每個元素都為int,的指標「int (*)[10][10]」)
char buf[10]; //buf is an array of 10 chars
(buf型別為指向char型態的指標「char*」)
const char* buf = "test"; //buf is an const pointer
(buf型別為指向char型態的常數指標「const char*」)

注意上面所有解析都是只單對這個變數代表的意義所解析的型別,如int x[10]雖然是個陣列,但是x代表是這個陣列的起始位址,所以這個位址的型別是int*依此類推。

※ pass by value V.S. pass an address by value

Example:

void write(int* tmp) (傳址呼叫函式)
{
*tmp = 20;
}
void show(int tmp) (傳值呼叫函式)
{
printf("%d\n", tmp);
}
int main()
{
int a = 10;
write(&a);
show(3);
return 0;
}

當主程式中呼叫副程式的時候會依順序執行下列動做:

1. 呼叫函式時把變數內容或是常數或是地址(實際參數)複製到Stack中
2. 主控權移交到函式手上
3. 函式的傳入值(形式參數)對映Stack中變數內容或是常數或是地址
4. 型別的契合(重超要), 可能會有隱含的資料轉型,就是因為型別的不契合才會自動轉型,或是記憶體存取錯誤就是因為指標的型別不契合,所以型別要正確
5. 執行遇到return時主控權移交回它的呼叫者手上
6. 函式執行結束後Stack內容變成無效值,剛剛對映Stack中的形式參數已變成無效用,但不一定會清除

重點整理

* 其實傳值呼叫和傳址呼叫都是把資料考到Stack中,只是一個是「值」一個是「址」
* 為什麼傳陣列的時候要傳「址」,為的是不浪費Stack空間且浪費複製時間
* 什麼時候用到傳值,用到傳值只為的是單一運算結果,如a+b+c回傳的只有一個答案
* 什麼時候用到傳址,用到傳值是了要修改不止一個運算結果,如將陣列每一個元素都+1,用傳值是沒有辦法做的
* 因為函式結束後Stack內容會無效,所以不可回傳Stack中某個變數的指標給呼叫者解參考(超重要)
* 在函式中使用malloc要到一塊記憶體,是從Heap中配置的空間,因此可以回傳此指標,因為和Stack沒有關係
* 使用malloc在函式中配置的記憶體空間,若沒有free回去的話,那一定要回傳此指標,讓呼叫者對此空間控制,否則遺失指標會造成memory leak

※ pass by value 的運作

由此圖中我們可以了解pass by value是如何運作的,也可以了解到return值對pass by value是多重要,若是沒有return tmp的話,那這函式做的一切都是徒然,做了白工,而且只能回傳單一值。

※ pass an address by value 的運作

由此圖中我們可以看到使用pass an address by value可以一次更改兩個變數的內容值,而且不需要return任何資料,所以需要更改一個值以上的資料我們必需要使用到傳遞指標讓函式來間接的幫我們修改資料內容。

※ 各式各樣的函式傳遞

* int (pass by value)(單一數值)

Example:

int add(int a)
{
return a+1;
}
int main()
{
int b = 1;
int data = add(b);
}

* int* (pass an address by value)(單一指標)

Example:

void add(int* a)
{
*a += 1;
}
int main()
{
int b = 1;
add(&b);
}

* int* (pass an address by value)(一維陣列)

Example:

void init(int* ptr, int size)
{
int i;
for (i = 0; i < size; ++i)
{
ptr[i] = 0;
}
}
int main()
{
int x[10];
init(x, 10);
}

* int (*)[10] (pass an address by value)(二維陣列)

Example:

void init(int (*ptr)[10], int size)
{
int i, j;
for (i = 0; i < size; ++i)
{
for (j = 0; j < 10; ++j)
{
ptr[i][j] = 0;
}
}
}
int main()
{
int x[10][10];
init(x, 10);
}

* char* (pass an address by value)(常數字串)

Example:

char getlastch(const char* p)
{
while (*p)
{
++p;
}
return *--p;
}
int main()
{
char ch = getlastch("address"); (輸出s)
}

* int** (pass an address by value)(雙指標)

Example:

void init(int** ptr, int size)
{
int i;
for (i = 0; i < size; ++i)
{
ptr[i] = (int*)malloc(sizeof(int) * 10);
(又將每個int*配出十個大小的int陣列)
}
}
int main()
{
int** x = (int**)malloc(sizeof(int*) * 10);
(配出int* x[10]的空間)
init(x, 10);
}

* char** (pass an address by value)(字串陣列)

Example:

void show(char** char_ary, int size);
int main()
{
char* ary[MAX] = {
"one",
"two",
"three"
};
show(ary, MAX);
return 0;
}
void show(char** char_ary, int size)
{
int i;
for (i = 0; i < size; ++i)
printf("%s\n", char_ary[i]);
}

在函式的傳遞中,對於型別的判定是非常重要的,傳遞實際參數不論是「值」或是「址」最重要的是,要非常了解這個「址」的型別,是二維的還是一維的,這都非常的重要,因為C語言在函式傳遞的時候,若發現你的型別不符合的話,會想辦法幫你自隱藏的型別轉換,所以要特別注意,傳遞時的型別,否則到時候指標加加減減解參考到錯誤的空間那你程式就準備當掉了,所以指標的快速好用,也帶給程式設計者一項很重要的任務,就是參考到的記憶體位址必須要完全正確才行。

0 意見: