目錄
函數是什么
庫函數
學習使用 strcpy 庫函數
自定義函數
寫一個函數能找出兩個整數中的最大值?
寫一個函數交換兩個整型變量的內容
牛刀小試
寫一個函數判斷一個整數是否是素數
寫一個函數判斷某一年是否是閏年
寫一個函數,實現一個整型有序數組的二分查找
函數的嵌套調用
函數的鏈式訪問
一段有趣的代碼
函數的聲明、調用和定義
函數的定義
函數調用
函數聲明?
函數遞歸?
什么是遞歸
遞歸的兩個必要條件?
一個簡單的遞歸
牛刀小試
要求使用函數遞歸實現:接受一個無符號整型值,按照順序打印他的每一位
要求寫一個函數:能實現求字符串的長度?
編寫函數,不允許創建臨時變量,求字符串的長度
函數是什么
在 C 語言里,函數和數學中的函數有相似之處
以數學函數?f(x)=2x+1?為例,當?x?取值不同時,函數會有不同的結果。比如?x=2?時,將?x=2?代入函數可得?f(2)=2×2+1=5;當?x=3?時,代入后得到?f(3)=2×3+1=7
同樣,這個數學函數也能用 C 語言中的函數來表示。我們可以定義一個 C 語言函數,讓它接收一個參數,在函數內部按照?2x+1?的規則進行計算并返回結果,從而實現與這個數學函數相同的功能
代碼演示:
int f(int x)
{return 2*x + 1;
}int main()
{int x = 0;scanf("%d", &x);printf("%d\n", f(x));return 0;
}
庫函數
在 C 語言里,為了方便使用常用功能,會將這些功能封裝成一個個函數,這些函數被稱為庫函數,像?printf
?函數、scanf
?函數、strlen
?函數等都屬于庫函數
需要注意的是,C 語言本身并沒有直接實現這些庫函數,而是制定了 C 語言的標準以及庫函數的約定。比如對于?scanf
?函數,C 語言標準規定了它的功能、函數名、參數和返回值等
而庫函數的具體實現通常由編譯器來完成,常見的編譯器如 VS2022 編譯器、gcc 編譯器等,它們會依據 C 語言標準對庫函數進行具體的編碼實現,這樣開發者就能在編程時直接使用這些庫函數了
學習使用 strcpy 庫函數
C/C++ 中的庫函數信息,都可以在?cplusplus.com?網站?上查詢到
如果想學習或了解某個庫函數的用法、參數含義及功能,直接在這個網站搜索對應的函數名即可,它是編程中查閱庫函數的實用資源
strcpy 庫函數
“destination” 代表目的地字符串,“source” 代表源頭字符串。當一個函數的返回值類型為?char*
?時,意味著該函數返回的是目的地字符串的首地址
strcpy
?是一個庫函數,從其文檔可知,它的功能是進行字符串拷貝。具體來說,就是把源頭字符串的數據復制到目的地字符串,并且會覆蓋目的地字符串原有的內容,同時還會將源頭字符串末尾的結束符?'\0'
?也一同復制過去
若要在代碼中使用?strcpy
?函數,需要包含相應的頭文件,頭文件代碼如下:
#include<string.h>
代碼演示:
char source[] = "hello world";
char destination[] = "xxxxxxxxxxxxxxxx";strcpy(destination, source);printf("%s\n", destination);
代碼驗證:
自定義函數
在編程中,自定義函數指的是開發者根據實際需求自己編寫的函數。它和 C 語言中的庫函數(如printf
、strlen
)一樣,都遵循相同的基本結構,包括:
- 返回類型:函數執行完畢后輸出的數據類型(如
int
、char*
); - 參數:函數執行時需要的輸入信息(可以沒有參數,也可以有多個);
- 返回值:通過
return
語句返回的結果(若無需返回結果,返回類型為void
)
寫一個函數能找出兩個整數中的最大值?
代碼演示:?
int get_max(int a, int b)
{return a > b ? a : b;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);int max = get_max(a, b);printf("%d\n", max);return 0;
}
寫一個函數交換兩個整型變量的內容
代碼演示:
void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交換前:a = %d;b = %d\n", a, b);Swap(&a, &b);printf("交換后:a = %d;b = %d\n", a, b);return 0;
}
在編寫函數交換兩個整型變量的值時,不能直接傳遞實參。這是因為在函數調用時,實參的值會被復制給形參,實參和形參分別占用不同的內存空間,也就是各自獨立的空間。在函數內部對形參進行操作,只會改變形參的值,而不會影響到實參原本的值
為了真正實現交換兩個實參的值,我們需要傳遞實參的地址。由于實參是?int
?類型,所以函數的形參要使用?int*
?類型來接收這些地址。int*
?類型的變量可以存儲整型變量的地址。在函數內部,通過?*
?這個解引用關鍵字,我們可以根據存儲的地址找到對應的實參,進而對實參的值進行修改,這樣就能實現兩個實參值的交換
牛刀小試
寫一個函數判斷一個整數是否是素數
代碼演示:
int is_prime(int tmp)
{if (tmp <= 1)return 0;for (int i = 2; i <= sqrt(tmp); i++){if (tmp % i == 0)return 0;}return 1;
}int main()
{int input = 0;scanf("%d", &input);if (is_prime(input))printf("is prime\n");elseprintf("not is prime\n");return 0;
}
代碼解析:
我們要編寫一個函數來判斷一個整數是否為素數。素數是指大于 1 且只能被 1 和自身整除的正整數。所以在判斷之前,首先要排除小于等于 0 的整數,因為它們顯然不符合素數的定義
該函數的返回規則是:返回 0 表示這個數不是素數,返回 1 表示這個數是素數
對于輸入的變量?input
,我們需要判斷它是否為素數。判斷的方法是,用?input
?對 2 到?input - 1
?之間的數進行取模運算。可以使用?for
?循環來遍歷這個區間內的所有數。如果在遍歷過程中,input
?對某個數取模的結果為 0,那就說明?input
?除了 1 和它本身之外,還能被其他數整除,那么它就不是素數,此時函數直接返回 0
不過,其實并不需要從 2 遍歷到?input - 1
,只需要遍歷 2 到?sqrt(input)
(sqrt
?是開平方函數)之間的數即可。這是因為如果一個數?input
?不是素數,那么它一定可以分解為兩個因數?m
?和?n
,即?input = m * n
,其中?m
?和?n
?中至少有一個小于等于?sqrt(input)
。所以,只要檢查到?sqrt(input)
?就可以判斷?input
?是否為素數了
如果?for
?循環執行完畢都沒有找到能整除?input
?的數,那就說明?input
?只能被 1 和它本身整除,即?input
?是素數,此時函數返回 1
寫一個函數判斷某一年是否是閏年
代碼演示:
int is_leap_year(int year)
{if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))return 1;elsereturn 0;
}int main()
{ int year = 0;scanf("%d", &year);is_leap_year(year);if (is_leap_year(year))printf("is leap year\n");elseprintf("not is leap year\n");return 0;
}
代碼解析:
閏年的判斷規則是能被 4 整除但不能被 100 整除,或能被 400 整除的年份
根據以上的規則編寫出對應的邏輯代碼,就能判斷某一年是否是潤年
寫一個函數,實現一個整型有序數組的二分查找
代碼演示:
int binary_search(int* parr, int size, int number)
{int left = 0;int right = size - 1;while (left <= right){int mid = (left + right) / 2;if (parr[mid] < number){left = mid + 1;}else if(parr[mid] > number){right = mid - 1;}else{return mid;}}return -1;
}int main()
{int arr[] = { 1,2,4,6,7,9,12,32,45,77,90 };int size = sizeof(arr) / sizeof(arr[0]);int input = 0;scanf("%d", &input);int ret = binary_search(arr, size, input);if (ret == -1)printf("number not found\n");elseprintf("number index is: %d\n", ret);return 0;
}
代碼解析:
我們要實現一個二分查找函數,二分查找是一種高效的查找算法,它的前提是數組必須是有序的。在函數調用時,需要傳遞三個參數:數組的指針?arr
,用于訪問數組元素;數組的元素個數?size
,這樣能確定查找范圍;要查找的整數?input
,即我們要在數組中找到的目標值
函數的返回值規則是:如果在數組中找到了目標值?input
,就返回該值在數組中的下標;如果沒有找到,就返回 -1,因為數組的下標最小是從 0 開始的, -1 可以作為一個明確的未找到的標識
- 初始化查找范圍:定義兩個變量,
left
?作為左下標,初始值設為 0,它指向數組的起始位置;right
?作為右下標,初始值設為?size - 1
,它指向數組的末尾位置 - 開始循環查找:使用?
while
?循環來進行查找,循環的條件是?left <= right
。只要滿足這個條件,就說明還有元素沒有被檢查過,查找過程可以繼續 - 計算中間下標:在每次循環內部,計算中間元素的下標?
mid
,計算公式為?mid = (left + right) / 2
。通過這個中間下標,我們可以將數組分成兩部分 - 比較中間元素與目標值:
- 如果?
input
?大于中間元素?arr[mid]
,說明目標值在數組的右半部分,此時更新?left
?為?mid + 1
,縮小查找范圍到右半部分 - 如果?
input
?小于中間元素?arr[mid]
,說明目標值在數組的左半部分,此時更新?right
?為?mid - 1
,縮小查找范圍到左半部分 - 如果?
input
?等于中間元素?arr[mid]
,說明已經找到了目標值,直接返回?mid
,也就是目標值在數組中的下標
- 如果?
- 未找到目標值:如果?
while
?循環結束后還沒有找到目標值,說明目標值不在數組中,此時返回 -1 即可
函數的嵌套調用
在編程中,當我們定義了多個函數后,有時會需要在一個函數的執行過程中,調用另一個函數來完成特定任務。這種?在一個函數內部調用另一個函數的方式,就是函數的嵌套調用
代碼演示:
void print()
{printf("hello world\n");
}void three_print()
{for (int i = 0; i < 3; i++){print();}
}int main()
{three_print();return 0;
}
在 three_print 函數中調用了 print 函數,這就是函數的嵌套調用?
函數的鏈式訪問
函數的鏈式訪問是一種編程技巧,它指的是將一個函數的返回值直接作為另一個函數的參數來使用
代碼演示:
printf("%d\n", strlen("abcdef"));
把 strlen 函數的返回值作為 printf 函數的參數,這就是函數的鏈式訪問?
函數的鏈式訪問可以讓代碼更加簡潔和緊湊,避免創建中間變量。不過,在使用鏈式訪問時也要注意代碼的可讀性,如果鏈式訪問的函數過多,可能會讓代碼變得難以理解和調試。所以,在實際編程中要根據具體情況合理使用函數的鏈式訪問?
一段有趣的代碼
代碼演示:
printf("%d", printf("%d", printf("%d", 43)));
問:最后在控制臺上輸出的結果是多少?
需明確?printf
?函數的返回值為其輸出的字符個數:
如:printf("%d", 1); 的返回值就是 1,printf("%d", 123); 的返回值就是 3
具體執行過程如下:
- 執行最內層?
printf("%d", 43)
,會先在控制臺輸出?43
(共 2 個字符),返回值為?2
- 中間層?
printf("%d", 2)
?接收內層返回值,輸出?2
(1 個字符),返回值為?1
- 最外層?
printf("%d", 1)
?接收中間層返回值,輸出?1
(1 個字符)
綜上,控制臺輸出結果為?4321
代碼驗證:
函數的聲明、調用和定義
函數的定義
int Add(int a, int b)
{return a + b;
}
函數 Add 的功能為計算兩個整數的和,其定義是實現該功能的具體代碼邏輯,即通過設定參數接收兩個整數輸入,在函數體內執行加法運算,并將運算結果作為返回值輸出,從而完成兩個整數相加功能的程序實現
函數調用
int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);int sum = Add(a, b);printf("%d\n", sum);return 0;
}
在 main 函數里使用 Add 函數的操作,在編程中被稱作函數調用,此操作可觸發 Add 函數執行其預設功能
函數聲明?
// 函數聲明
int Add(int a, int b);int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);// 函數調用int sum = Add(a, b);printf("%d\n", sum);return 0;
}// 函數定義
int Add(int a, int b)
{return a + b;
}
在 C 程序設計中,若自定義函數的定義位于 main 函數之后,則需在 main 函數執行前進行函數聲明
這是由于程序遵循從上至下的執行邏輯,編譯器在處理 main 函數時若未預先知曉自定義函數的存在,會因無法識別函數名稱而報錯
按照模塊化編程規范,函數聲明通常被放置在頭文件(.h)中,用于告知編譯器函數的參數類型和返回值類型;而函數的具體實現(定義)則集中存儲在對應的源文件(.c)中,這種分離式設計有助于代碼的組織、維護及復用,確保程序在編譯階段能夠正確解析函數調用關系
函數遞歸?
什么是遞歸
遞歸是函數直接或間接調用自身的編程方式。需定義基線條件(終止遞歸的條件)和遞歸條件(逐步逼近基線的邏輯),通過重復調用解決可分解為相似子問題的任務
遞歸的核心思維在于:把大事化小
遞歸的兩個必要條件?
- 基線條件(終止條件):必須存在至少一個無需遞歸調用的終止條件,用于退出遞歸過程,避免無限循環;
- 遞歸條件:函數需通過自身調用逐步分解問題,且每次遞歸調用必須更接近基線條件,確保問題規模遞減直至滿足終止條件
一個簡單的遞歸
int main()
{printf("hello\n");main();return 0;
}
若 main 函數直接調用自身,構成直接遞歸。由于該遞歸未設置基線條件(終止條件),程序會無限次遞歸調用自身,導致調用棧不斷增長
當棧空間被耗盡時,將引發棧溢出錯誤,造成程序異常終止。這一現象體現了遞歸中終止條件的必要性 —— 缺少該條件會破壞遞歸的收斂性,最終導致內存資源耗盡
牛刀小試
要求使用函數遞歸實現:接受一個無符號整型值,按照順序打印他的每一位
例如:
輸入:1234 ;輸入:1 2 3 4?
代碼演示:
void print_every_one(int n)
{if (n > 9){print_every_one(n / 10);}printf("%d ", n % 10);
}
代碼解析:
該函數實現按順序打印無符號整數每一位的功能,核心通過數學運算?%10
(取個位)和?/10
(去除個位)分解數字,并結合遞歸逐層處理
算法思路:
數字分解原理:
- 對于任意整數?
n
,n % 10
?可獲取其個位數字(如?123 % 10 = 3
) n / 10
?可去除個位,得到高位數字(如?123 / 10 = 12
)
通過重復這兩步,可逐位拆解整數的每一位
遞歸過程分析(以?n = 123
?為例):
首次調用與遞歸展開
- 初始調用?
print_every_one(123)
,因?123 > 9
,觸發遞歸調用?print_every_one(123 / 10 = 12)
- 第二次調用?
print_every_one(12)
,因?12 > 9
,繼續遞歸調用?print_every_one(12 / 10 = 1)
基線條件觸發(遞歸終止)
- 第三次調用?
print_every_one(1)
,此時?1 <= 9
,不滿足遞歸條件,跳過?if
?語句,直接執行?printf("%d ", 1 % 10)
,輸出?1
逐層返回與后續打印
- 返回上一層(第二次調用,
n = 12
),執行?printf("%d ", 12 % 10)
,輸出?2 - 再返回初始層(首次調用,
n = 123
),執行?printf("%d ", 123 % 10)
,輸出?3
最終輸出結果
控制臺按遞歸返回順序打印?1 2 3,實現從高位到低位的順序輸出
核心邏輯總結:
- 遞歸特性:通過不斷將問題規模縮小(
n / 10
),直至滿足基線條件(n <= 9
)時終止遞歸,再逐層返回處理當前層的打印邏輯 - 執行順序:遞歸調用時先處理高位(通過不斷剝離個位),返回時再依次打印低位,利用調用棧的后進先出特性,自然實現從高位到低位的順序輸出
要求寫一個函數:能實現求字符串的長度?
代碼演示:
int my_strlen(const char* s)
{int count = 0;while (*s != '\0'){count++;s++;}return count;
}
代碼解析:
此函數?my_strlen
?用于計算字符串的長度。參數?s
?是一個指向字符串首字符的指針
在 C 語言里,字符串以?'\0'
?作為結束標志。函數利用?while
?循環來遍歷字符串,只要當前指針?s
?所指向的字符不是?'\0'
,就表明還有字符需要統計
在循環內部,count
?變量用于統計字符串中字符的數量,每統計一個字符,count
?的值就加 1。同時,指針?s
?通過?s++
?操作指向下一個字符,從而實現對字符串的逐字符遍歷
當?s
?指向?'\0'
?時,意味著已經遍歷完整個字符串,此時循環結束,count
?變量中存儲的數值就是字符串中字符的總個數,也就是該字符串的長度,最后將其作為函數的返回值返回
編寫函數,不允許創建臨時變量,求字符串的長度
代碼演示:
int my_strlen(const char* s)
{if (*s == '\0')return 0;return 1 + my_strlen(s+1);
}
代碼解析:
函數參數與返回值
該函數接受一個指向?const char
?類型的指針?s
?作為參數,const
?表明此指針指向的字符串內容不能被修改。函數返回一個?int
?類型的值,即字符串的長度
遞歸終止條件
在函數內部,首先會檢查指針?s
?所指向的字符是否為?'\0'
。在 C 語言中,字符串以?'\0'
?作為結束標志。如果當前字符是?'\0'
,則意味著已經到達字符串的末尾,此時函數將返回 0。這是遞歸的終止條件,它確保了遞歸調用不會無限進行下去,避免出現棧溢出的錯誤
遞歸調用邏輯
若當前字符不是?'\0'
,說明還未遍歷完整個字符串。函數會將指針?s
?向后移動一位(s + 1
),使其指向下一個字符,然后再次調用?my_strlen
?函數,以計算從下一個字符開始的子字符串的長度。由于當前字符也是字符串的一部分,所以在遞歸調用返回的結果上加 1,表示當前字符的長度。最終將這個累加后的結果作為當前字符串的長度返回
遞歸調用示例
假設傳入的字符串為?"abc"
,遞歸調用過程如下:
- 第一次調用?
my_strlen("abc")
,當前字符為?'a'
,不是?'\0'
,則進行遞歸調用?1 + my_strlen("bc")
- 第二次調用?
my_strlen("bc")
,當前字符為?'b'
,不是?'\0'
,繼續遞歸調用?1 + my_strlen("c")
- 第三次調用?
my_strlen("c")
,當前字符為?'c'
,不是?'\0'
,再次遞歸調用?1 + my_strlen("")
- 第四次調用?
my_strlen("")
,此時當前字符為?'\0'
,滿足終止條件,返回 0 - 返回到第三次調用,
my_strlen("c")
?返回?1 + 0 = 1
- 返回到第二次調用,
my_strlen("bc")
?返回?1 + 1 = 2
- 返回到第一次調用,
my_strlen("abc")
?返回?1 + 2 = 3
通過這種遞歸的方式,函數逐步縮小問題規模,直到滿足終止條件,最終計算出整個字符串的長度