C語言歸納五-函數

目錄

一、比較字符串大小的代碼封裝成函數

二、形參和實參的區別

三、頭文件的講究

四、全局變量的講究(static的作用)

五、單獨的代碼塊

六、遞歸函數

6.1 斐波那契數列:

6.2 遞歸函數的空間開銷:

6.3 遞歸函數的時間開銷 clock_t CLOCKS_PER_SEC

七、需要注意的地方


一、比較字符串大小的代碼封裝成函數

int strcmp_alias(char *s1, char *s2){
    int i, result;
    for(i=0; (result = s1[i] - s2[i]) == 0; i++){
        if(s1[i] == '\0' || s2[i] == '\0'){
            break;
        }
    }
   
    return result;
}

二、形參和實參的區別

(1) 形參變量只有在函數被調用時纔會分配內存,調用結束後,立刻釋放內存,所以形參變量只有在函數內部有效,不能在函數外部使用。
(2)在函數調用過程中,形參的值發生改變並不會影響實參。

三、頭文件的講究

很多初學者認爲 stdio.h 中包含了函數定義(也就是函數體),只要有了頭文件就能運行,其實不然,頭文件中包含的都是函數聲明,而不是函數定義,函數定義都放在了其它的源文件中,這些源文件已經提前編譯好了,並以動態鏈接庫或者靜態鏈接庫的形式存在,只有頭文件沒有系統庫的話,在鏈接階段就會報錯,程序根本不能運行。

對於多個文件的程序,通常是將函數定義放到源文件(.c文件)中,將函數的聲明放到頭文件(.h文件)中,使用函數時引入對應的頭文件就可以,編譯器會在鏈接階段找到函數體。

四、全局變量的講究(static的作用)

全局變量的默認作用域是整個程序,也就是所有的代碼文件,包括源文件(.c文件)和頭文件(.h文件)。如果給全局變量加上 static 關鍵字,它的作用域就變成了當前文件,在其它文件中就無效了。

五、單獨的代碼塊

C語言還允許出現單獨的代碼塊,它也是一個作用域。請看下面的代碼:

#include <stdio.h>
int main(){
    int n = 22;  //編號①
    //由{ }包圍的代碼塊
    {
        int n = 40;  //編號②
        printf("block n: %d\n", n);
    }
    printf("main n: %d\n", n);
   
    return 0;
}

這裏有兩個 n,它們位於不同的作用域,不會產生命名衝突。{ } 的作用域比 main() 更小。

六、遞歸函數

6.1 斐波那契數列:

//遞歸計算斐波那契數
long fib(int n) {
    if (n <= 2) {
        return 1;
    }
    else {
        return fib(n - 1) + fib(n - 2);
    }
}

6.2 遞歸函數的空間開銷:

每個線程都擁有一個棧,如果一個程序包含了多個線程,那麼它就擁有多個棧。

對每個線程來說,棧能使用的內存是有限的,一般是 1M~8M,這在編譯時就已經決定了,程序運行期間不能再改變。如果程序使用的棧內存超出最大值,就會發生棧溢出(Stack Overflow)錯誤。

遞歸函數內部嵌套了對自身的調用,除非等到最內層的函數調用結束,否則外層的所有函數都不會調用結束。通俗地講,外層函數被卡主了,它要等待所有的內層函數調用完成後,它自己才能調用完成。

每一層的遞歸調用都會在棧上分配一塊內存,有多少層遞歸調用就分配多少塊相似的內存,所有內存加起來的總和是相當恐怖的,很容易超過棧內存的大小限制,這個時候就會導致程序崩潰。

例如,一個遞歸函數需要遞歸 10000 次,每次需要 1KB 的內存,那麼最終就需要 10MB 的內存。下面我們用遞歸的方式來求 1+2+3+ ...... + (n-1) + n 的值:

long sum(int n) {
    //爲了增加每次函數調用的內存,額外增加了一個無用的數組,它佔用 1KB 的內存
    int arr[250];
    if (n <= 1) {
        return n;
    } else {
        return  n + sum(n-1);
    }
}

這是因爲,每次遞歸調用都需要超過 1KB 的內存(僅僅數組就佔用了 1KB 內存),而要得到最終的結果需要 1000 次遞歸調用,這樣一來,所有內存的總和就超過了 1MB。

上面我們說過,Visual Studio 默認的棧內存只有 1MB,超過這個界限程序就無法運行了,只能讓它崩潰。使用其它的編譯器也許程序不會崩潰,讀者可以親自嘗試。

6.3 遞歸函數的時間開銷 clock_t CLOCKS_PER_SEC

每次調用函數都會在棧上分配內存,函數調用結束後再釋放這一部分內存,內存的分配和釋放都是需要時間的。

每次調用函數還會多次修改寄存器的值,函數調用結束後還需要找到上層函數的位置再繼續執行,這也是需要時間的。

所有的這些時間加在一起是非常恐怖的。

下面我們以「求斐波那契數」爲例來演示雙層遞歸的時間開銷。

#include <stdio.h>
#include <time.h>
// 遞歸計算斐波那契數
long fib(int n) {
    if (n <= 2) {
        return 1;
    }
    else {
        return fib(n - 1) + fib(n - 2);
    }
}
int main() {
    int a;
    clock_t time_start, time_end;
    printf("Input a number: ");
    scanf("%d", &a);
    time_start = clock();
    printf("Fib(%d) = %ld\n", a, fib(a));
    time_end = clock();
    printf("run time: %lfs\n", (double)(time_end - time_start)/ CLOCKS_PER_SEC );
    return 0;
}

運行結果:
Input a number: 42↙
Fib(42) = 267914296
run time: 13.137000s
可以看到,爲了求 42 的斐波那契數程序竟然運行了 13 秒,簡直讓人髮指。

最合適的辦法:使用迭代來替換遞歸函數,迭代函數實現時間近乎於0s。

七、需要注意的地方

(1)int b = a + 20;是具有運算功能的語句,要放在函數內部。

(2)標準C語言(ANSI C)定義了15個頭文件,成爲“C標準庫”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章