
文章目錄
- 函式是什么?
- 庫函式
- 自定義函式
- 函式的呼叫
- 函式的嵌套呼叫和鏈式訪問
- 函式的宣告和定義
- 函式遞回
函式是什么?
數學中我們常見到函式的概念,但是你了解C語言中的函式嗎? 維基百科中對函式的定義:子程式,
在計算機科學中,子程式(英語:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一個大型程式中的某部分代碼, 由一個或多個陳述句塊組成,它負責完成某項特定任務,而且相較于其他代 碼,具備相對的獨立性,
一般會有輸入引數并有回傳值,提供對程序的封裝和細節的隱藏,這些代碼通常被集成為軟體庫,
C語言函式的分類:
- 庫函式
- 自定義函式
庫函式
為什么會有庫函式?
- 我們知道在我們學習C語言編程的時候,總是在一個代碼撰寫完成之后迫不及待的想知道結果,想把這個結果列印到我們的螢屏上看看,這個時候我們會頻繁的使用一個功能:將資訊按照一定的格
式列印到螢屏上(printf), - 在編程的程序中我們會頻繁的做一些字串的拷貝作業(strcpy),
- 在編程是我們也計算,總是會計算n的k次方這樣的運算(pow),
像上面我們描述的基礎功能,它們不是業務性的代碼,我們在開發的程序中每個程式員都可能用的到,
為了支持可移植性和提高程式的效率,所以C語言的基礎庫中提供了一系列類似的庫函式,方便程式員
進行軟體開發,
庫函式學習:cplusplus.com

簡單的總結,C語言常用的庫函式都有:
- IO函式
- 字串操作函式
- 字符操作函式
- 記憶體操作函式
- 時間/日期函式
- 數學函式
- 其他庫函式
我們參照檔案,學習幾個庫函式:
strcpy
char * strcpy ( char * destination, const char * source );
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = "xxxxxxxxx";//目標空間
char arr2[] = "hello";
char* ret = strcpy(arr1, arr2);
printf("%s\n", arr1);
printf("%s\n", ret);
return 0;
}
運行結果:

在使用這個函式的時候,我們需要向函式引數傳入arr1和arr2兩個字串,同時該函式的回傳值是char* ,它會將拷貝后的arr1起始地址回傳給我們,可以選擇使用一個字符指標還接受它也可以直接使用,
memset
void * memset ( void * ptr, int value, size_t num );
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "hello world";//xxxxx world
int n = 5;
char* ret = (char* )memset(arr, 'x', n);
printf("%s\n", ret);
return 0;
}
運行結果:

memset是將ptr指向的記憶體塊的前num個位元組設定為指定值
自定義函式
如果庫函式能干所有的事情,那還要程式員干什么?
所有更加重要的是自定義函式,
自定義函式和庫函式一樣,有函式名,回傳值型別和函式引數, 但是不一樣的是這些都是我們自己來設計,這給程式員一個很大的發揮空間,
函式的組成:
ret_type fun_name(para1,…)
{
statment;//陳述句項
}
ret_type//回傳型別
fun_name//函式名
para//引數
下面舉個例子:
寫一個函式可以找出兩個整數中的最大值,
#include <stdio.h>
//get_max函式的設計
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
寫一個函式可以交換兩個整形變數的內容,
void Swap1(int x, int y) {
int t = 0;
t = x;
x = y;
y = t;
}
void Swap2(int* px, int* py){
int t = 0;
t = *px;
*px = *py;
*py = t;
}
int main(){
int a = 10;
int b = 20;
Swap1(a,b);
printf("Swap1:a=%d,b=%d\n", a, b);
Swap2(&a, &b);
printf("Swap2:a=%d,b=%d\n", a, b);
return 0;
}
函式的引數:
實際引數(實參):
真實傳給函式的引數,叫實參,實參可以是:常量、變數、運算式、函式等,無論實參是何種型別的量,在進行函式呼叫時,它們都必須有確定的值,以便把這些值傳送給形參,
形式引數(形參):
形式引數是指函式名后括號中的變數,因為形式引數只有在函式被呼叫的程序中才實體化(分配記憶體單元),所以叫形式引數,形式引數當函式呼叫完成之后就自動銷毀了,因此形式引數只在函式中有效,
上面Swap1和Swap2函式中的引數 x,y,px,py 都是形式引數,在main函式中傳給Swap1的num1,
num2和傳給Swap2函式的&num1,&num2是實際引數,

可以看到,Swap1函式是不能交換兩個值的,因為Swap1函式呼叫的時候,x、y擁有自己的空間,有了和實際引數一模一樣的值,是相當于對實參的一份臨時拷貝,在Swap1函式中改變值并不會影響實參的值,而Swap2函式將引數的地址傳入函式,這種傳參方式可以讓函式和函式外邊的變數建立起真正的聯系,在函式中使用地址改變形參的值就可以改變實參的值,
函式的呼叫
傳值呼叫
函式的形參和實參分別占有不同記憶體塊,對形參的修改不會影響實參,
傳址呼叫
傳址呼叫是把函式外部創建變數的記憶體地址傳遞給函式引數的一種呼叫函式的方式,
這種傳參方式可以讓函式和函式外邊的變數建立起正真的聯系,也就是函式內部可以直接操作函式外部的變數,
練習
- 寫一個函式可以判斷100到200有幾個素數,
#include <stdio.h>
#include <math.h>
int isPrime (int x)
{
if (x == 1 || x == 2) return 1;
int i = 0;
for (i = 2; i <= sqrt(x); i++)
{
if (x % i == 0)
return 0;
}
return 1;
}
int main()
{
int count = 0;
int i = 0;
for (i = 100; i <= 200; i++)
{
if (isPrime(i))
{
printf("%d ", i);
count++;
}
}
printf("\ncount = %d", count);
return 0;
}
- 寫一個函式判斷一年是不是閏年,
#include <stdio.h>
int is_leap_year(int x)
{
if ((x % 400 == 0) || ((x % 4 == 0) && (x % 100 != 0)))
return 1;
else
return 0;
}
int main()
{
int y;
for (y = 1000; y <= 2000; y++)
{
if (is_leap_year(y))
{
printf("%d ", y);
}
}
return 0;
}
- 寫一個函式,實作一個整形有序陣列的二分查找,
int binary_search(int arr[], int x, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (arr[mid] > x)
{
right = mid - 1;
}
else if (arr[mid] < x)
{
left = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
#include <stdio.h>
int main()
{
int k = 7;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int ret = binary_search(arr, k, sz);
if (ret= -1)
{
printf("找到了,下標是%d\n", ret);
}
else
{
printf("找不到\n");
}
}
函式的嵌套呼叫和鏈式訪問
函式和函式之間是可以有機的組合的,
嵌套呼叫
#include <stdio.h>
void new_line()
{
printf("hello\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}

以上代碼在main函式中呼叫three_line函式,在three_line函式中又呼叫了new_line函式,這就是函式的嵌套呼叫,
鏈式訪問
把一個函式的回傳值作為另外一個函式的引數,
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"world"));
printf("%d\n", ret);
return 0;
}
這個代碼用strcat函式的回傳值作為strlen函式的引數,這就是函式的鏈式訪問,
例子
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
上面代碼的運行結果是什么呢?

答案是4321,因為printf函式的回傳值是回傳所列印的元素個數,最內層的printf列印43,回傳值是2,次內層printf列印2,回傳值是1,最后最外層列印1,結果就為4321,
函式的宣告和定義
函式宣告
- 告訴編譯器有一個函式叫什么,引數是什么,回傳型別是什么,但是具體是不是存在,無關緊要,
- 函式的宣告一般出現在函式的使用之前,要滿足先宣告后使用,
- 函式的宣告一般要放在頭檔案中的,
函式定義
函式的定義是指函式的具體實作,交待函式的功能實作,
函式遞回
什么是遞回?
程式呼叫自身的編程技巧稱為遞回( recursion), 遞回做為一種演算法在程式設計語言中廣泛應用, 一個程序或函式在其定義或說明中有直接或間接呼叫自身的一種方法,它通常把一個大型復雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞回策略只需少量的程式就可描述出解題程序所需要的多次重復計算,大大地減少了程式的代碼量, 遞回的主要思考方式在于:把大事化小,
遞回的兩個必要條件
- 存在限制條件,當滿足這個限制條件的時候,遞回便不再繼續,
- 每次遞回呼叫之后越來越接近這個限制條件,
練習1:
接收一個整型值(無符號),按照順序列印它的每一位, 例如: 輸入:1234,輸出 1 2 3 4.
#include <stdio.h>
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
運行結果:

畫圖分析:

如果沒有if (n > 9)這個限制條件會怎么樣?

如果沒有if (n > 9)這個限制條件,編譯器就會報錯,因為函式中記憶體的開辟是在堆疊區的,如果沒有限制條件,就會無限遞回下去,就會無限為print函式開辟空間,堆疊的空間是有限的,最終導致堆疊滿,程式崩潰,

練習2:
撰寫函式不允許創建臨時變數,求字串的長度,
#include <stdio.h>
int Strlen(const char* str)
{
if (*str == '\0')
return 0;
else return 1 + Strlen(str + 1);
}
int main()
{
char* p = "abcdef";
int len = Strlen(p);
printf("%d\n", len);
return 0;
}
運行結果:

畫圖講解:

遞回與迭代
練習3:
求n的階乘,(不考慮溢位)
int factorial(int n)
{
if(n <= 1)
return 1;
else
return n* factorial(n-1);
}
但是當我們運行時會發現有問題:
- 在使用 fib 這個函式的時候如果我們要計算第50個斐波那契數字的時候特別耗費時間,
- 使用 factorial函式求10000的階乘(不考慮結果的正確性),程式會崩潰,
那我們如何改進呢?
在除錯 factorial 函式的時候,如果你的引數比較大,那就會報錯: `stack overflow(堆疊溢位) 這樣的資訊, 系統分配給程式的堆疊空間是有限的,但是如果出現了死回圈,或者(死遞回),這樣有可能導致一直開辟堆疊空間,最終產生堆疊空間耗盡的情況,這樣的現象我們稱為堆疊溢位,
那如何解決上述的問題:
- 將遞回改寫成非遞回,
- 使用static物件替代nonstatic區域物件,在遞回函式設計中,可以使用static物件替代nonstatic區域物件(即堆疊對 象),這不僅可以減少每次遞回呼叫和回傳時產生和釋放nonstatic物件的開銷,而且static物件還可以保存遞回呼叫的中間狀態,并且可為各個呼叫層所訪問,
下面用非遞回來實作階乘:
int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n ;
n -= 1;
}
return result;
}
提示:
- 許多問題是以遞回的形式進行解釋的,這只是因為它比非遞回的形式更為清晰,
- 但是這些問題的迭代實作往往比遞回實作效率更高,雖然代碼的可讀性稍微差些,
- 當一個問題相當復雜,難以用迭代實作時,此時遞回實作的簡潔性便可以補償它所帶來的運行時開銷,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/290776.html
標籤:其他
下一篇:行程、埠與套接字
