C/C++

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=&#039;*&#039;;
    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() 的區域變數存放,此過程稱為「傳值」。

留下一個回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *