第六章 函數
6.1 概述
6.1.1 函數分類
C 程序是由函數組成的,我們寫的代碼都是由主函數 main()
開始執行的。函數是 C 程序的基本模塊,是用於完成特定任務的程序代碼單元。
從函數定義的角度看,函數可分爲系統函數和用戶定義函數兩種:
- 系統函數,即庫函數:這是由編譯系統提供的,用戶不必自己定義這些函數,可以直接使用它們,如我們常用的打印函數
printf()
。 - 用戶定義函數:用以解決用戶的專門需要。
6.1.2 函數的作用
- 函數的使用可以省去重複代碼的編寫,降低代碼重複率
// 求兩數的最大值
int max(int a, int b)
{
if (a > b){
return a;
}
else{
return b;
}
}
int main()
{
// 操作1 ……
// ……
int a1 = 10, b1 = 20, c1 = 0;
c1 = max(a1, b1); // 調用max()
// 操作2 ……
// ……
int a2 = 11, b2 = 21, c2 = 0;
c2 = max(a2, b2); // 調用max()
// ……
return 0;
}
- 函數可以讓程序更加模塊化,從而有利於程序的閱讀,修改和完善
假如我們編寫一個實現以下功能的程序:讀入一行數字;對數字進行排序;找到它們的平均值;打印出一個柱狀圖。如果我們把這些操作直接寫在main()
裏,這樣可能會給用戶感覺代碼會有點凌亂。但,假如我們使用函數,這樣可以讓程序更加清晰、模塊化:
#include <stdio.h>
int main()
{
float list[50];
// 這裏只是舉例,函數還沒有實現
readlist(list, 50);
sort(list, 50);
average(list, 50);
bargraph(list, 50);
return 0;
}
這裏我們可以這麼理解,程序就像公司,公司是由部門組成的,這個部門就類似於C程序的函數。默認情況下,公司就是一個大部門( 只有一個部門的情況下 ),相當於C程序的main()
函數。如果公司比較小( 程序比較小 ),因爲任務少而簡單,一個部門即可( main()函數 )勝任。但是,如果這個公司很大( 大型應用程序 ),任務多而雜,如果只是一個部門管理( 相當於沒有部門,沒有分工 ),我們可想而知,公司管理、運營起來會有多混亂,不是說這樣不可以運營,只是這樣不完美而已,如果根據公司要求分成一個個部門( 根據功能封裝一個一個函數 ),招聘由行政部門負責,研發由技術部門負責等,這樣就可以分工明確,結構清晰,方便管理,各部門之間還可以相互協調。
6.2 函數的定義
6.2.1 函數定義格式
函數定義的一般形式:
返回類型 函數名(形式參數列表)
{
數據定義部分;
執行語句部分;
}
6.2.2 函數名字、形參、函數體、返回值
1.函數名
理論上是可以隨意起名字,最好起的名字見名知意,應該讓用戶看到這個函數名字就知道這個函數的功能。注意,函數名的後面有個圓換號(),代表這個爲函數,不是普通的變量名。
2.形參列表
在定義函數時指定的形參,在未出現函數調用時,它們並不佔內存中的存儲單元,因此稱它們是形式參數或虛擬參數,簡稱形參,表示它們並不是實際存在的數據,所以,形參裏的變量不能賦值。
void max(int a = 10, int b = 20) // error, 形參不能賦值
{
}
在定義函數時指定的形參,必須是,類型+變量的形式:
//1: right, 類型+變量
void max(int a, int b)
{
}
//2: error, 只有類型,沒有變量
void max(int, int)
{
}
//3: error, 只有變量,沒有類型
int a, int b;
void max(a, b)
{
}
在定義函數時指定的形參,可有可無,根據函數的需要來設計,如果沒有形參,圓括號內容爲空,或寫一個void關鍵字:
// 沒形參, 圓括號內容爲空
void max()
{
}
// 沒形參, 圓括號內容爲void關鍵字
void max(void)
{
}
3.函數體
花括號{ }裏的內容即爲函數體的內容,這裏爲函數功能實現的過程,這和以前的寫代碼沒太大區別,以前我們把代碼寫在main()
函數裏,現在只是把這些寫到別的函數裏。
4.返回值
函數的返回值是通過函數中的return
語句獲得的,return
後面的值也可以是一個表達式。
a.儘量保證return語句中表達式的值和函數返回類型是同一類型。
int max() // 函數的返回值爲int類型
{
int a = 10;
return a;// 返回值a爲int類型,函數返回類型也是int,匹配
}
b.如果函數返回的類型和return
語句中表達式的值不一致,則以函數返回類型爲準,即函數返回類型決定返回值的類型。對數值型數據,可以自動進行類型轉換。
double max() // 函數的返回值爲double類型
{
int a = 10;
return a;// 返回值a爲int類型,它會轉爲double類型再返回
}
注意:如果函數返回的類型和return
語句中表達式的值不一致,而它又無法自動進行類型轉換,程序則會報錯。
c.return
語句的另一個作用爲中斷return
所在的執行函數,類似於break
中斷循環、switch
語句一樣。
int max()
{
return 1;// 執行到,函數已經被中斷,所以下面的return 2無法被執行到
return 2;// 沒有執行
}
d.如果函數帶返回值,return
後面必須跟着一個值,如果函數沒有返回值,函數名字的前面必須寫一個void
關鍵字,這時候,我們寫代碼時也可以通過return
中斷函數(也可以不用),只是這時,return
後面不帶內容( 分號“;
”除外)。
void max()// 最好要有void關鍵字
{
return; // 中斷函數,這個可有可無
}
6.3 函數的調用
定義函數後,我們需要調用此函數才能執行到這個函數裏的代碼段。這和main()函數不一樣,main()爲編譯器設定好自動調用的主函數,無需人爲調用,我們都是在main()函數裏調用別的函數,一個 C 程序裏有且只有一個main()函數。
6.3.1函數執行流程
#include <stdio.h>
void print_test()
{
printf("this is for test\n");
}
int main()
{
print_test(); // print_test函數的調用
return 0;
}
- 進入
main()
函數 - 調用
print_test()
函數:
a. 它會在main()
函數的前尋找有沒有一個名字叫“print_test
”的函數定義;
b. 如果找到,接着檢查函數的參數,這裏調用函數時沒有傳參,函數定義也沒有形參,參數類型匹配;
c. 開始執行print_test()
函數,這時候,main()函數裏面的執行會阻塞( 停 )在print_test()
這一行代碼,等待print_test()
函數的執行。 print_test()
函數執行完( 這裏打印一句話 ),main()
纔會繼續往下執行,執行到return 0
, 程序執行完畢。
6.3.2 函數的形參和實參
- 形參出現在函數定義中,在整個函數體內都可以使用,離開該函數則不能使用。
- 實參出現在主調函數中,進入被調函數後,實參也不能使用。
- 實參變量對形參變量的數據傳遞是“值傳遞”,即單向傳遞,只由實參傳給形參,而不能由形參傳回來給實參。
- 在調用函數時,編譯系統臨時給形參分配存儲單元。調用結束後,形參單元被釋放。
- 實參單元與形參單元是不同的單元。調用結束後,形參單元被釋放,函數調用結束返回主調函數後則不能再使用該形參變量。實參單元仍保留並維持原值。因此,在執行一個被調用函數時,形參的值如果發生改變,並不會改變主調函數中實參的值。
6.3.3 無參函數調用
如果是調用無參函數,則不能加上“實參”,但括號不能省略。
// 函數的定義
void test()
{
}
int main()
{
// 函數的調用
test(); // right, 圓括號()不能省略
test(250); // error, 函數定義時沒有參數
return 0;
}
6.3.4有參函數調用
a.如果實參表列包含多個實參,則各參數間用逗號隔開。
// 函數的定義
void test(int a, int b)
{
}
int main()
{
int p = 10, q = 20;
test(p, q); // 函數的調用
return 0;
}
b.實參與形參的個數應相等,類型應匹配(相同或賦值兼容)。實參與形參按順序對應,一對一地傳遞數據。
c.實參可以是常量、變量或表達式,無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值,以便把這些值傳送給形參。所以,這裏的變量是在圓括號( )外面定義好、賦好值的變量。
// 函數的定義
void test(int a, int b)
{
}
int main()
{
// 函數的調用
int p = 10, q = 20;
test(p, q); // right
test(11, 30 - 10); // right
test(int a, int b); // error, 不應該在圓括號裏定義變量
return 0;
}
6.3.5 函數返回值
a.如果函數定義沒有返回值,函數調用時不能寫void
關鍵字,調用函數時也不能接收函數的返回值。
// 函數的定義
void test()
{
}
int main()
{
// 函數的調用
test(); // right
void test(); // error, void關鍵字只能出現在定義,不可能出現在調用的地方
int a = test(); // error, 函數定義根本就沒有返回值
return 0;
}
b)如果函數定義有返回值,這個返回值我們根據用戶需要可用可不用,但是,假如我們需要使用這個函數返回值,我們需要定義一個匹配類型的變量來接收。
// 函數的定義, 返回值爲int類型
int test()
{
}
int main()
{
// 函數的調用
int a = test(); // right, a爲int類型
int b;
b = test(); // right, 和上面等級
char *p = test(); // 雖然調用成功沒有意義, p爲char *, 函數返回值爲int, 類型不匹配
// error, 必須定義一個匹配類型的變量來接收返回值
// int只是類型,沒有定義變量
int = test();
// error, 必須定義一個匹配類型的變量來接收返回值
// int只是類型,沒有定義變量
int test();
return 0;
}
6.4 函數的聲明
如果使用用戶自己定義的函數,而該函數與調用它的函數(即主調函數)不在同一文件中,或者函數定義的位置在主調函數之後,則必須在調用此函數之前對被調用的函數作聲明。
所謂函數聲明,就是在函數尚在未定義的情況下,事先將該函數的有關信息通知編譯系統,相當於告訴編譯器,函數在後面定義,以便使編譯能正常進行。
注意:一個函數只能被定義一次,但可以聲明多次。
#include <stdio.h>
int max(int x, int y); // 函數的聲明,分號不能省略
// int max(int, int); // 另一種方式
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函數的調用
printf("num_max = %d\n", num_max);
return 0;
}
// 函數的定義
int max(int x, int y)
{
return x > y ? x : y;
}
函數定義和聲明的區別:
- 定義是指對函數功能的確立,包括指定函數名、函數類型、形參及其類型、函數體等,它是一個完整的、獨立的函數單位。
- 聲明的作用則是把函數的名字、函數類型以及形參的個數、類型和順序(注意,不包括函數體)通知編譯系統,以便在對包含函數調用的語句進行編譯時,據此對其進行對照檢查(例如函數名是否正確,實參與形參的類型和個數是否一致)。
6.5 main函數與exit函數
在main
函數中調用exit
和return
結果是一樣的,但在子函數中調用return
只是代表子函數終止了,在子函數中調用exit
,那麼程序終止。
#include <stdio.h>
#include <stdlib.h>
void fun()
{
printf("fun\n");
//return;
exit(0);
}
int main()
{
fun();
while (1);
return 0;
}
6.6 多文件(分文件)編程
6.6.1 分文件編程
- 把函數聲明放在頭文件
xxx.h
中,在主函數中包含相應頭文件 - 在頭文件對應的
xxx.c
中實現xxx.h
聲明的函數
6.6.2 防止頭文件重複包含
當一個項目比較大時,往往都是分文件,這時候有可能不小心把同一個頭文件 include
多次,或者頭文件嵌套包含。
a.h 中包含 b.h :
#include "b.h"
b.h 中包含 a.h:
#include "a.h"
main.c 中使用其中頭文件:
#include "a.h"
int main()
{
return 0;
}
編譯上面的例子,會出現如下錯誤:
爲了避免同一個文件被include
多次,C/C++中有兩種方式,一種是 #ifndef
方式,一種是 #pragma once
方式。
方法一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
// 聲明語句
#endif
方法二:
#pragma once
// 聲明語句