學習書籍: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 — 排列組合](http://localhost/wordpress/2021/03/18/c%ef%bc%9a%e9%81%9e%e8%bf%b4-permutation-%e6%8e%92%e5%88%97%e7%b5%84%e5%90%88/ "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() 的區域變數存放,此過程稱為「傳值」。