C 程式設計/過程和函式
在 C 程式設計中,所有可執行程式碼都駐留在一個函式中。請注意,其他程式語言可能會區分“函式”、“子例程”、“子程式”、“過程”或“方法”——在 C 中,這些都是函式。函式是任何高階程式語言的基本特性,它使我們能夠透過將任務分解成更小、更易於管理的程式碼塊來處理大型複雜的任務。
在更低級別,函式不過是在計算機記憶體中儲存函式相關指令的記憶體地址。在原始碼中,此記憶體地址通常會賦予一個描述性的名稱,程式設計師可以使用此名稱呼叫函式並執行從函式起始地址開始的指令。與函式相關的指令通常被稱為程式碼塊。在函式指令執行完畢後,函式可以返回一個值,程式碼執行將從緊接在函式初始呼叫之後的指令處繼續。如果您現在還不能理解,別擔心。一開始理解計算機內部最底層發生的事件可能會讓人困惑,但隨著您 C 程式設計技能的提升,最終會變得非常直觀。
現在,您只需要知道函式及其相關程式碼塊通常會多次執行(呼叫),從程式執行期間的幾個不同位置呼叫。
舉個簡單的例子,假設您正在編寫一個計算給定 (x,y) 點到 x 軸和 y 軸距離的程式。您需要計算整數 x 和 y 的絕對值。我們可以這樣寫(假設我們在任何庫中都沒有預定義的絕對值函式)
#include <stdio.h>
/*this function computes the absolute value of a whole number.*/
int abs(int x)
{
if (x>=0) return x;
else return -x;
}
/*this program calls the abs() function defined above twice.*/
int main()
{
int x, y;
printf("Type the coordinates of a point in 2-plane, say P = (x,y). First x=");
scanf("%d", &x);
printf("Second y=");
scanf("%d", &y);
printf("The distance of the P point to the x-axis is %d. \n Its distance to the y-axis is %d. \n", abs(y), abs(x));
return 0;
}
下面的例子說明了函式作為過程的使用。這是一個簡單的程式,它要求學生提供三個不同課程的成績,並告訴他們是否通過了課程。在這裡,我們建立了一個名為 check() 的函式,可以根據需要呼叫任意次數。該函式使我們不必為學生上的每門課都編寫相同的指令集。
#include<stdio.h>
/*the 'check' function is defined here.*/
void check(int x)
{
if (x<60)
printf("Sorry! You will need to try this course again.\n");
else
printf("Enjoy your vacation! You have passed.\n");
}
/*the program starts here at the main() function, which calls the check() function three times.*/
int main()
{
int a, b, c;
printf("Type your grade in Mathematics (whole number). \n");
scanf("%d", &a);
check(a);
printf("Type your grade in Science (whole number). \n");
scanf("%d", &b);
check(b);
printf("Type your grade in Programming (whole number). \n");
scanf("%d", &c);
check(c);
/* this program should be replaced by something more meaningful.*/
return 0;
}
請注意,在上面的程式中,'check' 函式沒有結果值。它只執行一個過程。
這就是函式的真正用途。
更多關於函式的資訊
[edit | edit source]將函式概念化為工廠中的機器很有用。在機器的輸入側,您輸入“原材料”或要由機器處理的輸入資料。然後機器開始工作,並將完成的產品,“返回值”,吐到機器的輸出側,您可以收集並用於其他目的。
在 C 中,您必須告訴機器預期處理哪些原材料以及您希望機器返回什麼型別的完成產品。如果您為機器提供了與預期不同的原材料,或者試圖返回與您告訴機器生產的產品不同的產品,C 編譯器將丟擲錯誤。
請注意,函式不需要接收任何輸入。它也不需要返回任何內容給我們。如果我們將上面的例子修改為在 check 函式中詢問使用者他們的成績,則無需將成績值傳遞到函式中。並且請注意,check 函式不會將值傳遞回來。該函式只是在螢幕上列印一條訊息。
您應該熟悉與函式相關的某些基本術語
- 使用另一個函式 g 的函式 f 被稱為呼叫 g。例如,f 呼叫 g 來列印十個數的平方。f 被稱為呼叫者函式,g 被稱為被呼叫者。
- 我們傳送給函式的輸入稱為其引數。當我們宣告函式時,我們描述引數,這些引數決定了哪些型別的引數可以傳遞到函式中。我們在函式名稱旁邊的括號中向編譯器描述這些引數。
- 返回某種答案給 f 的函式 g 被稱為返回該答案或值。例如,g 返回其引數的總和。
在 C 中編寫函式
[edit | edit source]透過例子學習總是好的。讓我們編寫一個返回數字平方的函式。
int square(int x)
{
int square_of_x;
square_of_x = x * x;
return square_of_x;
}
要了解如何編寫這樣的函式,看看這個函式整體執行的操作可能會有所幫助。它接收一個int, x,並對其進行平方,將其儲存在變數 square_of_x 中。現在此值將被返回。
函式宣告開頭的第一個 int 是函式返回的資料型別。在本例中,當我們對整數進行平方時,我們得到一個整數,我們正在返回此整數,因此我們寫int作為返回值型別。
接下來是函式的名稱。使用有意義且描述性的函式名稱是一個良好的習慣。將函式命名為它所編寫的操作可能會有所幫助。在本例中,我們將函式命名為“square”,因為它就是做這件事的——它對數字進行平方。
接下來是函式的第一個也是唯一一個引數,一個int, 它將在函式中被稱為 x。這是函式的輸入。
花括號之間的部分是函式的實際核心。它聲明瞭一個名為 square_of_x 的整數變數,用於儲存 x 的平方值。請注意,變數 square_of_x 只能在函式內部使用,而不能在外部使用。我們將在後面學習更多關於這類內容的資訊,並且我們會看到這種屬性非常有用。
然後,我們將 x 乘以 x,或者 x 的平方,賦值給變數 square_of_x,這就是這個函式的全部內容。接下來是一個return語句。我們想要返回 x 的平方值,因此我們必須說這個函式返回變數 square_of_x 的內容。
我們的花括號要關閉,我們已經完成了宣告。
以更簡潔的方式編寫,這段程式碼與上面的程式碼執行完全相同的功能
int square(int x)
{
return x * x;
}
請注意,這應該看起來很熟悉——您實際上一直在編寫函式——main 是一個始終編寫的函式。
一般而言
[edit | edit source]一般來說,如果我們想宣告一個函式,我們寫
type name(type1 arg1, type2 arg2, ...)
{
/* code */
}
我們之前說過,函式可以不接收任何引數,也可以不返回任何內容,或者兩者兼而有之。如果我們想要讓函式不返回任何內容,我們應該寫什麼呢?我們使用 C 的void關鍵字。void基本上意味著“無”——所以如果我們想要編寫一個不返回任何內容的函式,例如,我們寫
void sayhello(int number_of_times)
{
int i;
for(i=1; i <= number_of_times; i++) {
printf("Hello!\n");
}
}
請注意,上面函式中沒有return語句。由於沒有,我們寫void作為返回值型別。(實際上,可以在過程中使用return關鍵字在過程結束之前返回呼叫者,但不能像函式一樣返回值。)
不接收任何引數的函式呢?如果我們想要這樣做,我們可以例如寫
float calculate_number(void)
{
float to_return=1;
int i;
for(i=0; i < 100; i++) {
to_return += 1;
to_return = 1/to_return;
}
return to_return;
}
請注意,此函式不接收任何輸入,而只是返回此函式計算出的一個數字。
當然,您也可以將 void 返回和 void 引數組合在一起以獲得有效的函式。
遞迴
[edit | edit source]這是一個簡單的函式,它執行無限迴圈。它列印一行並呼叫自身,這再次列印一行並再次呼叫自身,這種情況會一直持續下去,直到堆疊溢位並且程式崩潰。函式呼叫自身稱為遞迴,通常您會有一個條件語句在有限的小步數後停止遞迴。
// don't run this!
void infinite_recursion()
{
printf("Infinite loop!\n");
infinite_recursion();
}
可以這樣進行簡單的檢查。請注意 ++depth 用於在將值傳遞到函式之前進行遞增。或者,您可以在遞迴呼叫之前在單獨的一行上進行遞增。如果您說 print_me(3,0);,該函式將列印 Recursion 行 3 次。
void print_me(int j, int depth)
{
if(depth < j) {
printf("Recursion! depth = %d j = %d\n",depth,j); //j keeps its value
print_me(j, ++depth);
}
}
遞迴最常用於目錄樹掃描、查詢連結串列的結尾、解析資料庫中的樹結構以及分解數字(和查詢素數)等任務。
靜態函式
[edit | edit source]如果函式只能從宣告它的檔案中呼叫,則將其宣告為靜態函式是合適的。當函式被宣告為靜態時,編譯器將知道以一種防止從其他檔案中的程式碼呼叫函式的方式編譯目標檔案。示例
static int compare( int a, int b )
{
return (a+4 < b)? a : b;
}
使用 C 函式
[edit | edit source]我們現在可以編寫函式,但我們如何使用它們呢?當我們編寫 main 時,我們將函式放在包含 main 的大括號之外。
當我們想使用該函式時,比如,使用我們的calculate_number函式,我們可以寫類似下面這樣的程式碼
float f;
f = calculate_number();
如果函式接受引數,我們可以寫類似下面這樣的程式碼
int square_of_10;
square_of_10 = square(10);
如果函式不返回值,我們可以簡單地寫
say_hello();
因為我們不需要變數來捕獲它的返回值。
C 標準庫中的函式
[edit | edit source]雖然 C 語言本身不包含函式,但它通常與 C 標準庫連結在一起。要使用此庫,您需要在 C 檔案的頂部新增一個 #include 指令,它可能是來自 C89/C90 的以下指令之一
可用的函式是
| <assert.h> | <limits.h> | <signal.h> | <stdlib.h> |
|---|---|---|---|
|
|
|
|
| <ctype.h> | <locale.h> | <stdarg.h> | <string.h> |
|
|
|
|
| errno.h | math.h | stddef.h | time.h |
|
|
|
|
| float.h | setjmp.h | stdio.h | |
|
|
|
|
可變長引數列表
[edit | edit source]具有可變長引數列表的函式是可以接受可變數量引數的函式。C 標準庫中的一個例子是printf函式,它可以根據程式設計師的使用方式接受任意數量的引數。
C 程式設計師很少需要編寫新的具有可變長引數列表的函式。如果他們想傳遞大量內容給一個函式,他們通常會定義一個結構來儲存所有這些內容——也許是一個連結串列,或者一個數組——並將這些資料放在引數中呼叫該函式。
但是,您偶爾會發現需要編寫支援可變長引數列表的新函式。要建立一個可以接受可變長引數列表的函式,您必須首先包含標準庫標頭檔案stdarg.h. 接下來,像往常一樣宣告函式。接下來,將省略號("...")作為最後一個引數新增。這指示編譯器,接下來將是一個可變列表的引數。例如,以下函式宣告用於返回數字列表的平均值的函式
float average (int n_args, ...);
請注意,由於可變長引數的工作方式,我們必須在引數中以某種方式指定可變長引數部分中元素的數量。在average函式中,這是透過一個名為n_args的引數來實現的。在printf函式中,這是透過您在為該函式提供的引數中第一個字串中指定的格式程式碼來實現的。
現在函式已經被宣告為使用可變長引數,我們接下來必須編寫執行函式中實際工作的程式碼。要訪問儲存在我們average函式的可變長引數列表中的數字,我們必須首先宣告一個變數來表示該列表本身
va_list myList;
該va_list型別是在stdarg.h標頭檔案中宣告的型別,它基本上允許您跟蹤您的列表。但是,要開始實際使用myList,我們必須首先為它分配一個值。畢竟,僅僅宣告它本身並不會做任何事情。為此,我們必須呼叫va_start,它實際上是stdarg.h中定義的宏。在va_start的引數中,您必須提供您計劃使用的va_list變數,以及出現在您函式宣告中省略號之前的最後一個變數的名稱
#include <stdarg.h>
float average (int n_args, ...)
{
va_list myList;
va_start (myList, n_args);
va_end (myList);
}
現在myList已經準備好使用,我們終於可以開始訪問儲存在其中的變數。為此,使用va_arg宏,它會從列表中彈出下一個引數。在va_arg的引數中,提供您正在使用的va_list變數,以及您要訪問的變數應具有的原始資料型別(例如int, char)
#include <stdarg.h>
float average (int n_args, ...)
{
va_list myList;
va_start (myList, n_args);
int myNumber = va_arg (myList, int);
va_end (myList);
}
透過從可變長引數列表中彈出n_args個整數,我們可以設法找到數字的平均值
#include <stdarg.h>
float average (int n_args, ...)
{
va_list myList;
va_start (myList, n_args);
int numbersAdded = 0;
int sum = 0;
while (numbersAdded < n_args) {
int number = va_arg (myList, int); // Get next number from list
sum += number;
numbersAdded += 1;
}
va_end (myList);
float avg = (float)(sum) / (float)(numbersAdded); // Find the average
return avg;
}
透過呼叫average (2, 10, 20),我們得到10和20的平均值,也就是15.