C語言的基本組成單位—函數


前言

 函數是C語言的基本組成單位,他是模塊化設計的主要構成單元。

 為什么使用函數?

 一個程序可能會用到很多次,如果每次都寫這樣一段重復的代碼,不但費時費力、容易出錯,而且交給別人時也很麻煩,所以C語言提供了一個功能,允許我們將常用的代碼以固定的格式封裝(包裝)成一個獨立的模塊,只要知道這個模塊的名字就可以重復使用它,這個模塊就叫做函數(Function)。

 函數的本質是一段可以重復使用的代碼,這段代碼被提前編寫好了,放到了指定的文件中,使用時直接調取即可。使用函數設計程序,可以使得程序更具有模塊化結構,並且使程序更加簡潔明了,提高程序的易讀性與可維護性。函數還可以將一些多次重復使用的程序模塊封裝。


函數的定義


函數的相關概念


函數的分類


按照定義類型划分

  • 主函數

 主函數的調用名稱為main(),是C語言最主要的函數,具有唯一性(即任何一個C語言程序有且只有一個main函數),是程序的入口函數。

  • 庫函數

 C語言自帶的函數稱為庫函數(Library Function),是由開發人員編寫封裝后嵌入到C編譯系統中直接被用戶調用。庫(Library)是編程中的一個基本概念,可以簡單地認為它是一系列函數的集合,在磁盤上往往是一個文件夾。C語言自帶的庫稱為標准庫(Standard Library),其他公司或個人開發的庫稱為第三方庫(Third-Party Library)。

  • 用戶自定義函數

 用戶自定義函數由用戶定義,用於完成某些特定功能的函數片段,為便於維護和程序執行,將這些程序段進行封裝為函數的形式,與主函數對應,並通常稱之為子函數。


按照是否具有返回值划分

 既然函數可以處理數據,那就有必要將處理結果告訴我們,所以很多函數都有返回值(Return Value)。所謂返回值,就是函數的執行結果。函數返回值有固定的數據類型(int、char、float等),用來接收返回值的變量類型要一致。

char str[] = "I love you!";
int len = strlen(str);

//strlen() 的處理結果是字符串 str 的長度,是一個整數,通過 len 變量來接收

 有些函數調用后能返回某種類型的數值,而有些函數不可以,按照能否返回數值可以分為返回值類型函數和無返回類型函數,返回值類型函數在定義的時候應該指定返回指定的類型,即在定義時函數名前面的類型說明符,若為void 則說明該函數為無返回值類型。如果為空,則默認為int型函數.


按照函數是否帶參數划分

 函數的一個明顯特征就是使用時帶括號( ),有必要的話,括號中還要包含數據或變量,稱為參數(Parameter)。函數是一段可以重復使用的代碼,用來獨立地完成某個功能,它可以接收用戶傳遞的數據,也可以不接收。接收用戶數據的函數在定義時要指明參數,不接收用戶數據的不需要指明,根據這一點可以將函數分為有參函數和無參函數

 有參函數:函數定義時應該在小括號內包含形參,函數調用時應該將實參傳遞到函數內部用於計算。
 無參函數:無參函數即函數定義和調用時都不帶參數的函數類型。


形參和實參的區別

  • 形參(形式參數)
    在函數定義中出現的參數可以看做是一個占位符,它沒有數據,只能等到函數被調用時接收傳遞進來的數據,所以稱為形式參數,簡稱形參。

  • 實參(實際參數)
    函數被調用時給出的參數包含了實實在在的數據,會被函數內部的代碼使用,所以稱為實際參數,簡稱實參。

  • 形參和實參的功能是傳遞數據,發生函數調用時,實參的值會傳遞給形參。


形參和實參的區別和聯系
  1. 形參變量只有在函數被調用時才會分配內存,調用結束后,立刻釋放內存,所以形參變量只有在函數內部有效,不能在函數外部使用。

  2. 實參可以是常量、變量、表達式、函數等,無論實參是何種類型的數據,在進行函數調用時,它們都必須有確定的值,以便把這些值傳送給形參,所以應該提前用賦值、輸入等辦法使實參獲得確定值。

  3. 實參和形參在數量上、類型上、順序上必須嚴格一致,否則會發生“類型不匹配”的錯誤。當然,如果能夠進行自動類型轉換,或者進行了強制類型轉換,那么實參類型也可以不同於形參類型。

  4. 函數調用中發生的數據傳遞是單向的,只能把實參的值傳遞給形參,而不能把形參的值反向地傳遞給實參;換句話說,一旦完成數據的傳遞,實參和形參就再也沒有瓜葛了,所以,在函數調用過程中,形參的值發生改變並不會影響實參。

  5. 形參和實參雖然可以同名,但它們之間是相互獨立的,互不影響,因為實參在函數外部有效,而形參在函數內部有效。

這里給出一個例子,供大家學習:


函數的返回值

函數的返回值是指函數被調用之后,執行函數體中的代碼所得到的結果,通過 return 語句返回

return 語句的一般形式為:

return 表達式;

或

return (表達式);

有沒有( )都是正確的,一般也不寫( )

注意的點
  1. 沒有返回值的函數為空類型,用void表示。一旦函數的返回值類型被定義為 void,就不能再接收它的值了。凡不要求返回值的函數都應定義為 void 類型

  2. return 語句可以有多個,可以出現在函數體的任意位置,但是每次調用函數只能有一個 return 語句被執行,所以只有一個返回值。

  3. 函數一旦遇到 return 語句就立即返回,后面的所有語句都不會被執行到了。從這個角度看,return 語句還有強制結束函數執行的作用。

  4. return 語句是提前結束函數的唯一辦法。return 后面可以跟一份數據,表示將這份數據返回到函數外面;return 后面也可以不跟任何數據,表示什么也不返回,僅僅用來結束函數。


函數的定義


無參函數的定義

如果函數不接收用戶傳遞的數據,那么定義時可以不帶參數。

dataType  functionName()
{
    //body
}


dataType 是返回值類型,它可以是C語言中的任意數據類型,例如 int、float、char 等。
functionName 是函數名,它是標識符的一種,命名規則和標識符相同。函數名后面的括號( )不能少。
body 是函數體,它是函數需要執行的代碼,是函數的主體部分。即使只有一個語句,函數體也要由{ }包圍。
如果有返回值,在函數體中使用 return 語句返回。return 出來的數據的類型要和 dataType 一樣。

舉個栗子:通過函數計算從 1 加到 100 的結果

#include <stdio.h>

int sum()
{
    int i, sum=0;
    for(i=1; i<=100; i++)
    {
        sum+=i;
    }
    return sum;
}

int main()
{
    int a = sum();
    printf("%d\n", a);
    return 0;
}

運行結果:
5050

補充:

函數不能嵌套定義,main 也是一個函數定義,所以要將 sum 放在 main 外面。

函數必須先定義后使用,所以 sum 要放在 main 前面。

注意:

main 是函數定義,不是函數調用。

當可執行文件加載到內存后,系統從 main 函數開始執行,即系統會調用我們定義的 main 函數。

//無返回值函數

有的函數不需要返回值,或者返回值類型不確定(很少見),那么可以用 void 表示.

void是C語言中的一個關鍵字,表示“空類型”或“無類型”,絕大部分情況下也就意味着沒有 return 語句.


C語言有參函數的定義

如果函數需要接收用戶傳遞的數據,那么定義時就要帶上參數。

dataType  functionName( dataType1 param1, dataType2 param2 ... )
{
    //body
}

dataType1 param1, dataType2 param2 ...是參數列表。
函數可以只有一個參數,也可以有多個,多個參數之間由,分隔。
參數本質上也是變量,定義時要指明類型和名稱。
與無參函數的定義相比,有參函數的定義僅僅是多了一個參數列表

重點

數據通過參數傳遞到函數內部進行處理,處理完成以后再通過返回值告知函數外部。

舉個栗子:計算從 m 加到 n 的結果

#include <stdio.h>
int sum(int m, int n)
{
    int i, sum=0;
    for(i=m; i<=n; i++)
    {
        sum+=i;
    }
    return sum;
}
int main()
{
    int begin = 5, end = 86;
    int result = sum(begin, end);
    printf("%d\n",result);
    return 0;
}

運行結果:
3731

定義 sum() 時,參數 m、n 的值都是未知的;
調用 sum() 時,將 begin、end 的值分別傳遞給 m、n,這和給變量賦值的過程是一樣的

函數定義時給出的參數稱為形式參數,簡稱形參;函數調用時給出的參數(也就是傳遞的數據)稱為實際參數,簡稱實參。

函數調用時,將實參的值傳遞給形參,相當於一次賦值操作。

原則上講,實參的類型和數目要與形參保持一致。如果能夠進行自動類型轉換,或者進行了強制類型轉換,那么實參類型也可以不同於形參類型.

函數不能嵌套定義

C語言不允許函數嵌套定義;也就是說,不能在一個函數中定義另外一個函數,必須在所有函數之外定義另外一個函數。

main() 也是一個函數定義,也不能在 main() 函數內部定義新函數


函數的調用與聲明


函數的調用

函數調用(Function Call),就是使用已經定義好的函數。

一般形式:

functionName(param1, param2, param3 ...);

functionName 是函數名稱,param1, param2, param3 ...是實參列表。

實參可以是常數、變量、表達式等,多個實參用逗號,分隔。

//函數作為表達式中的一項出現在表達式中
//函數作為一個單獨的語句
//函數作為調用另一個函數時的實參

函數的嵌套調用

函數調用的基本形式

函數名(實參表列);//如果函數沒有參數,實參表列可以省略

函數名是函數定義的函數名稱,成為被調函數。

函數不能嵌套定義,但可以嵌套調用,也就是在一個函數的定義或調用過程中允許出現對另外一個函數的調用。

如果一個函數 A() 在定義或調用過程中出現了對另外一個函數 B() 的調用,那么我們就稱 A() 為主調函數或主函數,稱 B() 為被調函數。

當主調函數遇到被調函數時,主調函數會暫停,CPU 轉而執行被調函數的代碼;被調函數執行完畢后再返回主調函數,主調函數根據剛才的狀態繼續往下執行。

一個C語言程序的執行過程可以認為是多個函數之間的相互調用過程,它們形成了一個或簡單或復雜的調用鏈條。這個鏈條的起點是 main(),終點也是 main()。當 main() 調用完了所有的函數,它會返回一個值(例如return 0;)來結束自己的生命,從而結束整個程序。

函數是一個可以重復使用的代碼塊,CPU 會一條一條地挨着執行其中的代碼,當遇到函數調用時,CPU 首先要記錄下當前代碼塊中下一條代碼的地址(假設地址為 0X1000),然后跳轉到另外一個代碼塊,執行完畢后再回來繼續執行 0X1000 處的代碼。整個過程相當於 CPU 開了一個小差,暫時放下手中的工作去做點別的事情,做完了再繼續剛才的工作。

舉個栗子:

#include<stdio.h>

//函數聲明
void f1();
void f2();
void f3();
void f4();

void f1()
{
  printf("從前有座山,");
  f2();
}

void f2()
{
  printf("山上有座廟,");
  f3();
}

void f3()
{
  printf("山上有個老和尚講故事,");
  f4();
}

void f4()
{
  printf("講的什么呢?\n");
  f1();
}

int main()
{
  f1();
  return 0;
}

從上面栗子我們可以吃出來一點函數調用的韻味.
由於我們還沒詳細講解庫函數,這里運行速度太快,先將就一下


函數聲明

 C語言代碼由上到下依次執行,原則上函數定義要出現在函數調用之前,否則就會報錯,在編寫程序時,經常會在函數定義之前使用它們,這個時候就需要提前聲明。(聲明(Declaration),就是告訴編譯器我要使用這個函數,現在沒有找到它的定義不要緊,不要報錯,稍后把定義補上).

函數聲明的格式相當於去掉函數定義中的函數體,並在最后加上分號;

dataType  functionName( dataType1 param1, dataType2 param2 ... );

或不寫形參,只寫數據類型:

dataType  functionName( dataType1, dataType2 ... );

函數聲明給出了函數名、返回值類型、參數列表(重點是參數類型)等與該函數有關的信息,稱為函數原型(Function Prototype)。

函數原型的作用是告訴編譯器與該函數有關的信息,讓編譯器知道函數的存在,以及存在的形式,即使函數暫時沒有定義,編譯器也知道如何使用它。有了函數聲明,函數定義就可以出現在任何地方了,甚至是其他文件。

函數聲明的位置:

函數聲明的作用是告訴編譯器有一個已經定義好的子函數可以調用,所以常常把函數聲明放在函數頭部,而將函數定義放在調用位置函數之后。

還是上一個栗子(再撿回來):

#include<stdio.h>

//函數聲明
void f1();
void f2();
void f3();
void f4();

void f1()
{
  printf("從前有座山,");
  f2();
}

void f2()
{
  printf("山上有座廟,");
  f3();
}

void f3()
{
  printf("山上有個老和尚講故事,");
  f4();
}

void f4()
{
  printf("講的什么呢?\n");
  f1();
}

int main()
{
  f1();
  return 0;
}

從這里可以看到函數f1調用f2,f2調用f3……的時候是可以調用成功的。
原因就在於函數前面多了函數聲明區,f1才知道:哦,原來后面有一個f2……
大家可以嘗試把函數聲明區注釋掉,在調試程序,看看有沒有問題。
  • 使用函數聲明的原因:

對於單個源文件的程序,通常是將函數定義放到 main() 的后面,將函數聲明放到 main() 的前面,這樣就使得代碼結構清晰明了,主次分明。使用者往往只關心函數的功能和函數的調用形式,很少關心函數的實現細節,將函數定義放在最后,就是盡量屏蔽不重要的信息,凸顯關鍵的信息。將函數聲明放到 main() 的前面,在定義函數時也不用關注它們的調用順序了,哪個函數先定義,哪個函數后定義,都無所謂了。


全局變量與局部變量

不僅對於形參變量,C語言中所有的變量都有自己的作用域。決定變量作用域的是變量的定義位置。


局部變量

定義在函數內部的變量稱為局部變量(Local Variable),它的作用域僅限於函數內部, 離開該函數后就是無效的,再使用就會報錯。

int f1(int a)
{
    int b,c;  //a,b,c僅在函數f1()內有效
    return a+b+c;
}

int main()
{
    int m,n;  //m,n僅在函數main()內有效
    return 0;
}

說明

  1. 在 main 函數中定義的變量也是局部變量,只能在 main 函數中使用;
    同時,main 函數中也不能使用其它函數中定義的變量。main 函數也是一個函數,與其它函數地位平等。

  2. 形參變量、在函數體內定義的變量都是局部變量。實參給形參傳值的過程也就是給局部變量賦值的過程。

  3. 可以在不同的函數中使用相同的變量名,它們表示不同的數據,分配不同的內存,互不干擾,也不會發生混淆。

  4. 在語句塊中也可定義變量,它的作用域只限於當前語句塊。


全局變量

在所有函數外部定義的變量稱為全局變量(Global Variable),它的作用域默認是整個程序,也就是所有的源文件,包括 .c 和 .h 文件。

舉個栗子

int a, b;  //全局變量
void func1()
{
    //TODO:
}

float x,y;  //全局變量

int func2()
{
    //TODO:
}

int main()
{
    //TODO:
    return 0;
}

//注釋:
a、b、x、y 都是在函數外部定義的全局變量。
C語言代碼是從前往后依次執行的,由於 x、y 定義在函數 func1() 之后,所以在 func1() 內無效;
而 a、b 定義在源程序的開頭,所以在 func1()、func2() 和 main() 內都有效。

局部變量和全局變量的綜合

實例:

#include <stdio.h>

int n = 10;  //全局變量

void f1()
{
    int n = 20;  //局部變量
    printf("f1 n: %d\n", n);
}

void f2(int n)
{
    printf("f2 n: %d\n", n);
}

void f3()
{
    printf("f3 n: %d\n", n);
}

int main()
{
    int n = 30;  //局部變量
    func1();
    func2(n);
    func3();
    //代碼塊由{}包圍
    {
        int n = 40;  //局部變量
        printf("block n: %d\n", n);
    }
    printf("main n: %d\n", n);
    return 0;
}

//運行結果:
f1 n: 20
f2 n: 30
f3 n: 10
block n: 40
main n: 30

補充說明:

代碼中雖然定義了多個同名變量 n,但它們的作用域不同,在內存中的位置(地址)也不同,所以是相互獨立的變量,互不影響,不會產生重復定義(Redefinition)錯誤。

  1. 對於 f1(),輸出結果為 20,顯然使用的是函數內部的 n,而不是外部的 n;f2() 也是相同的情況。

當全局變量和局部變量同名時,在局部范圍內全局變量被“屏蔽”,不再起作用。或者說,變量的使用遵循就近原則,如果在當前作用域中存在同名變量,就不會向更大的作用域中去尋找變量。

  1. f3() 輸出 10,使用的是全局變量,因為在 f3() 函數中不存在局部變量 n,所以編譯器只能到函數外部,也就是全局作用域中去尋找變量 n。

  2. 由{ }包圍的代碼塊也擁有獨立的作用域,printf() 使用它自己內部的變量 n,輸出 40。

  3. C語言規定,只能從小的作用域向大的作用域中去尋找變量,而不能反過來,使用更小的作用域中的變量。對於 main() 函數,即使代碼塊中的 n 離輸出語句更近,但它仍然會使用 main() 函數開頭定義的 n,所以輸出結果是 30。


作用域

作用域(Scope),就是變量的有效范圍,就是變量可以在哪個范圍以內使用。變量的作用域由變量的定義位置決定,在不同位置定義的變量,它的作用域是不一樣的。


局部變量

 在函數內部定義的變量,它的作用域也僅限於函數內部,出了函數就不能使用了,我們將這樣的變量稱為局部變量(Local Variable)。函數的形參也是局部變量,也只能在函數內部使用。

重點:

  • main() 也是一個函數,在 main() 內部定義的變量也是局部變量,只能在 main() 函數內部使用。
  • 形參也是局部變量,將實參傳遞給形參的過程,就是用實參給局部變量賦值的過程,和a=b; 這樣的賦值沒有什么區別。

全局變量

C語言允許在所有函數的外部定義變量,這樣的變量稱為全局變量(Global Variable)。

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

在一個函數內部修改全局變量的值會影響其它函數,全局變量的值在函數內部被修改后並不會自動恢復,它會一直保留該值,直到下次被修改。

全局變量也是變量,變量只能保存一份數據,一旦數據被修改了,原來的數據就被沖刷掉了,再也無法恢復了,所以不管是全局變量還是局部變量,一旦它的值被修改,這種影響都會一直持續下去,直到再次被修改。


變量命名規則

 C語言規定,在同一個作用域中不能出現兩個名字相同的變量,否則會產生命名沖突;但是在不同的作用域中,允許出現名字相同的變量,它們的作用范圍不同,彼此之間不會產生沖突。

  • 不同函數內部可以出現同名的變量,不同函數是不同的局部作用域;
  • 函數內部和外部可以出現同名的變量,函數內部是局部作用域,函數外部是全局作用域。
  • 不同函數內部的同名變量是兩個完全獨立的變量,它們之間沒有任何關聯,也不會相互影響。
  • 函數內部的局部變量和函數外部的全局變量同名時,在當前函數這個局部作用域中,全局變量會被“屏蔽”,不再起作用,即函數內部使用的是局部變量,而不是全局變量。
  • 變量的使用遵循就近原則,如果在當前的局部作用域中找到了同名變量,就不會再去更大的全局作用域中查找。另外,只能從小的作用域向大的作用域中去尋找變量,而不能反過來,使用更小的作用域中的變量。

遞歸函數

一個函數在它的函數體內調用它自身稱為遞歸調用,這種函數稱為遞歸函數。執行遞歸函數將反復調用其自身,每調用一次就進入新的一層,當最內層的函數執行完畢后,再一層一層地由里到外退出。

我們給出遞歸函數的調用轉移圖:

實例分析:計算階乘

#include <stdio.h>

long f(int n) 
{
    if (n == 0 || n == 1) 
    {
        return 1;
    }
    else 
    {
        return f(n - 1) * n;  // 遞歸
    }
}
int main()
{
    int a;
    scanf("%d", &a);
    printf("%ld\n",f(a));
    return 0;
}

//運行結果:
5↙
120

f() 就是一個典型的遞歸函數。調用 f() 后即進入函數體,只有當\(n==0\)\(n==1\)時函數才會執行結束,否則就一直調用它自身。

由於每次調用的實參為 n-1,即把 n-1 的值賦給形參 n,所以每次遞歸實參的值都減 1,直到最后 n-1 的值為 1 時再作遞歸調用,形參 n 的值也為1,遞歸就終止了,會逐層退出。


遞歸進入

  1. 第一次遞歸:調用 f(5)。當進入子函數體后,形參 n 的值為 5,不等於 0 或 1,所以執行f(n-1) * n,即f(4) * 5。為了求這個結果,先調用 f(4),並暫停其他操作。即在得到 f(4) 的結果之前,不能進行其他操作。

  2. 第二次遞歸:調用 f(4) 時,實參為 4,形參 n 也為 4,不等於 0 或 1,繼續執行f(n-1) * n,也即執行f(3) * 4。為了求這個結果,又必須先調用 f(3)。

  3. 以此類推,進行四次遞歸調用后,實參的值為 1,會調用 f(1)。此時能夠直接得到常量 1 的值,並把結果 return。

層次/層數 實參/形參 調用形式 需要計算的表達式 需要等待的結果
1 n=5 f(5) f(4) * 5 f(4) 的結果
2 n=4 f(4) f(3) * 4 f(3) 的結果
3 n=3 f(3) f(2) * 3 f(2) 的結果
4 n=2 f(2) f(1) * 2 f(1) 的結果
5 n=1 f(1) 1

遞歸的退出

當遞歸進入到最內層的時候,遞歸就結束了,就開始逐層退出了,也就是逐層執行 return 語句。

  1. n 的值為 1 時達到最內層,此時 return 出去的結果為 1,即 f(1) 的調用結果為 1。

  2. 有了 f(1) 的結果,就可以返回上一層計算f(1) * 2的值了。此時得到的值為 2,return 出去的結果也為 2,也即 f(2) 的調用結果為 2。

  3. 得到 f(4) 的調用結果后,就可以返回最頂層。f(4) 的結果為 24,表達式f(4) * 5的結果為 120,此時 return 得到的結果也為 120。

層次/層數 調用形式 需要計算的表達式 從內層遞歸得到的結果
(內層函數的返回值) 表達式的值
(當次調用的結果)
5 f(1) 1
4 f(2) f(1) * 2 f(1) 的返回值,是 1
3 f(3) f(2) * 3 f(2) 的返回值,是 2
2 f(4) f(3) * 4 f(3) 的返回值,是 6
1 f(5) f(4) * 5 f(4) 的返回值,是 24

遞歸的條件

每一個遞歸函數都應該只進行有限次的遞歸調用,否則永遠也不能退出了。

要想讓遞歸函數逐層進入再逐層退出,則需要:

  • 存在限制條件,當符合這個條件時遞歸便不再繼續。
  • 每次遞歸調用之后越來越接近這個限制條件

經典實例:漢諾塔

下面分析轉自醬紫君

三個盤子的漢諾塔:

四個盤子的漢諾塔:

由此我們可以推出:\(a_{n}=2\cdot a_{n-1}+1\)

則需要完成的有三步:

  • 把上面的\(n-1\)號盤子移動到緩沖區
  • 把最大的盤子從起點移到終點
  • 然后把緩沖區的\(n-1\)號盤子也移到終點
void move (int n, char from, char buffer, char to)
{
    if (n == 1) 
    {
        printf("Move %d from %d to %d\n",n,from,to);
    }
    else 
    {
        move (n-1, from, to, buffer); //a->b,c為緩沖
        move (1, from, buffer, to);   //a->c,b為緩沖
        move (n-1, buffer, from, to); //b->c,a為緩沖
    }
}

//最后補充一個有意思的問題,配個圖:

End


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM