C 語言學習筆記 (七):函數用法
學習書籍:C 語言學習手冊 第四版。作者: 洪維恩
這是一篇記錄自己學習 C 語言的過程,算是給自己看的筆記,所以這裡面的內容,是我整理書中我認為重要的部分,然後用自己的方式重新寫一遍,如果有圖,我會理解完,再自己畫出來,內容肯定會和課本上有出入,若有錯誤,或是理解錯的地方,希望能讓我知道。
函數是 C 語言的基本模組,讓程式能夠用模組化的方式,來簡化主程式的結構。現在許多程式語言,例如 Java、Python …等等,也都有讓人撰寫函數的語法,也可以說這些程式語言背後也是有數不盡的函數組成,代表模組式的程式撰寫,是非常重要的思維。
#include<stdio.h>
#include<stdlib.h>
void say_hi(void);
int main(void){
say_hi();
return 0;
}
void say_hi(void){
printf("hi\n");
return;
}
從上面範例可知,函數的宣告包含函數資料型態、函數名稱、引數名稱及其形態。
以剛剛 say_hi() 函數為例:
- 函數宣告
- 函數定義
函數基本架構
從上面範例可以知道,要讓編譯器知道程式裡有函數,必須先進行函數原型 (prototype) 宣告,再來編寫函數主體。
- 函數宣告
函數回傳型態 函數名稱(引數型態 引數名稱, 引數型態 引數名稱, ...);
- 函數定義
函數回傳型態 函數名稱(引數型態 引數名稱, 引數型態 引數名稱, ...){
變數宣告;
敘述主體;
return 回傳值;
}
根據上面的架構,撰寫一個具有加法功能的函數:
int add(int a, int b);
int add(int a, int b){
int sum = 0;
sum = a + b;
return sum;
}
呼叫函數
若有回傳值。可以設定變數來接收回傳值。
變數 = 函數名稱(引數);
若是"沒有"回傳值或是"不接收"回傳值,可以直接打上函數名稱以及需要輸入的引數。
函數名稱(引數);
若函數不需要輸入引數的話,在呼叫函數時,括號中是不用打任何字,括號中間是空的。例如剛剛的
say_hi()
括號中間是空的,但是括號還是要打出來。
了解函數架構及呼叫方法後,就可以來實際撰寫完整的加法程式了。
#include<stdio.h>
#include<stdlib.h>
int add(int a, int b);
int main(void){
int sum, a, b;
a = 1;
b = 5;
sum = add(a,b);
return 0;
}
int add(int a, int b){
int sum = 0;
sum = a + b;
return sum;
}
從一開始的範例到剛剛的加法器的範例,都是將函數寫在 main 的後面。如果今天將函數寫在前面,會發生什麼事呢?
#include<stdio.h>
#include<stdlib.h>
int add(int a, int b){
int sum = 0;
sum = a + b;
return sum;
}
int main(void){
int sum, a, b;
a = 1;
b = 5;
sum = add(a,b);
return 0;
}
如果將函數寫在 main 前面的話,就不用函數原型宣告了,編譯器一樣可以成功執行程式。
更多函數範例
- 字元列印函數 display(ch, n)
#include<stdio.h>
#include<stdlib.h>
void display(char ,int);
int main(void){
char ch='*';
int n=8;
display(ch,n);
return 0;
}
void display(char ch , int n){
int i;
for(i=0; i<=n; i++){
printf("%c", ch);
}
printf("\n");
return;
}
- 絕對值函數 abs(n)
#include<stdio.h>
#include<stdlib.h>
int abs(int);
int main(void){
int n=-8;
printf("%d", abs(n));
return 0;
}
int abs(int n){
if(n<0){
return -n;
}else{
return n;
}
}
- 次方函數 power(x, n)
#include<stdio.h>
#include<stdlib.h>
double power(double, int);
int main(void){
double pow;
int a, b;
a = 2;
b = 5;
pow = power(a,b);
printf("%10.4f", pow);
return 0;
}
double power(double base, int n){
int i;
double pow=1.0;
for(i=1; i<=n; i++){
pow = pow*base;
}
return pow;
}
- 同時使用多個函數
#include<stdio.h>
#include<stdlib.h>
void fac(int);
void sum(int);
int main(void){
fac(5);
sum(5);
return 0;
}
void sum(int a){
int sum=0;
for(int i=1; i<=a; i++){
sum = sum + i;
// sum += i // 也可以簡化成這樣
}
printf("sum=%d\n", sum);
}
void fac(int a){
int fac=1;
for(int i=1; i<=a; i++){
fac = fac * i;
// fac *= i // 也可以簡化成這樣
}
printf("fac=%d\n", fac);
}
- 函數互相呼叫
萊布尼茲公式:估算圓周率 $\pi$ 的值。
使用互相呼叫函數,將下面的公式,寫成函數形式。
4 \sum^{n}_{k=1} \frac{(-1)^{k-1}}{2k-1}
#include<stdio.h>
#include<stdlib.h>
double Leibniz(int);
double power(double, int);
int main(void){
int i;
for(i=1; i<=10000; i++){
printf("Leibniz(%d) = %12.10f\n", i, Leibniz(i));
}
return 0;
}
double Leibniz(int n){
int k;
double sum=0.;
for(k=1; k<=n; k++){
sum = sum + power(-1.0, k-1)/(2*k-1);
// sum += i // 也可以簡化成這樣
}
return 4*sum;
}
double power(double base, int n){
int i;
double pow=1.0;
for(i=1; i<=n; i++){
pow = pow*base;
}
return pow;
}
遞迴函數
C 語言也有遞迴的機制。所謂遞迴就是函數呼叫自己本身。
最常舉得例子就是階乘函數 (factorial function,n!),其公式如下:
fac(n) = \underbrace{1\times2\times...\times n-1}_{fac(n-1)} \times n
= n\times fac(n-1)
可以降上面式子換成遞迴形式
fac(n) =
\begin{cases}
1\times2\times...\times n; n\ge1
\\
1; n=0
\end{cases}
#include<stdio.h>
#include<stdlib.h>
int fac(int);
int main(void){
printf("fac=%d\n", fac(4));
return 0;
}
int fac(int n){
if(n>1){
return (n*fac(n-1));
}else{
return 1;
}
}
關於遞迴的範例,還可以去看我之前寫得另一篇文章,C:遞迴 permutation — 排列組合,這個也是使用遞迴來實現排列組合。
區域、全域、靜態變數
- 區域變數
宣告在函數裡的變數。
#include<stdio.h>
#include<stdlib.h>
void check(void);
int main(void){
int a = 10;
printf("a in main() = %d\n", a);
check();
printf("a in main() = %d\n", a);
return 0;
}
void check(void){
int a = 30;
printf("a in check() = %d\n", a);
return;
}
從上面範例可知,雖然分別在主函數 main() 及 副函數 check() 建立了 "變數 a",但是他們存在不同記憶體空間,裡面的值也不相同,也無法相互影響。
- 全域變數
宣告在函數外的變數。
#include<stdio.h>
#include<stdlib.h>
int a;
void check(void);
int main(void){
a = 10;
printf("a in main() = %d\n", a);
check();
printf("a in main() = %d\n", a);
return 0;
}
void check(void){
a = 30;
printf("a in check() = %d\n", a);
return;
}
和一開始的區域變數不同,我們將 "變數 a " 宣告最外面之後,不論是主函數 main() 及 副函數 check() "變數 a " 都在相同記憶體空間,裡面的值也相同,也會相互影響。
如果宣告一個全域變數,又宣告了區域變數,區域變數將會取代掉全域變數。
#include<stdio.h>
#include<stdlib.h>
int a=100;
void check(void);
int main(void){
int a = 10;
printf("a in main() = %d\n", a);
check();
printf("a in main() = %d\n", a);
return 0;
}
void check(void){
a = a+30;
printf("a in check() = %d\n", a);
return;
}
可以看到上面 main() 中,變數 a 並不會被 check() 影響,一樣維持 10,代表這邊的變數 a 是區域變數;而 check() 中的變數 a 則是讀取全域變數的 a,因此得到的結果會是 130。
- 靜態變數
靜態變數和區域變數類似,都在函數裡宣告,但不同的是,靜態變數在編譯時,就已經配置好記憶體空間,因此當主控權不在函數上,還是可以將變數的值保留下來。
靜態變數的宣告方式:
static 變數的資料型態 變數名稱;
靜態變數的使用範例:撰寫一個函數,裡面宣告變數 a 為靜態變數,每當呼叫一次函數就會加上 100。
#include<stdio.h>
#include<stdlib.h>
void check(void);
int main(void){
check();
check();
check();
return 0;
}
void check(void){
static int a = 30;
// int a = 30;
printf("a in check() = %d\n", a);
a += 100;
return;
}
可以看到每次呼叫 check() 時,都不是從 30 開始,而是前一次加完 100 的結果,這表示變數 a,並沒有消失,還存在記憶體中。若是將 static 拿掉,這樣每次呼叫 check() 時,就只會顯示 30,上次的結果不會保存。
引數傳遞機制
引數的機制都是「傳值」(pass by value) 的方式。由下面程式來進行解說:
#include<stdio.h>
#include<stdlib.h>
void add10(int a, int b);
int main(void){
int a=5, b=10;
printf("before add10, a=%d b=%d\n", a, b);
add10(a,b);
printf(" after add10, a=%d b=%d\n", a, b);
return 0;
}
void add10(int a, int b){
a += 10;
b += 10;
printf(" in add10, a=%d b=%d\n", a, b);
}
上面程式裡,主函數 main() 會宣告出 a、b 兩個變數,副函數 add10(),會接收 a、b 兩個變數作為引數。
主函數 main() 確實傳送 a、b 變數到副函數 add10() 的 a、b 引數,也可以看到確實會進行加 10 的動作,但回到主程式的 a、b 變數,其值還是沒有改變,因此可以知道引數的機制是先拷貝變數值,再傳到 add10() 的區域變數存放,此過程稱為「傳值」。