維系C世界框架的英雄
上一章我們剛講完C世界中函式的呼叫方式,而接下來我們將進一步了解函式的嵌套呼叫和鏈式訪問等函式的其他內容
如果你想要想知道函式之前的故事,可以通過觀看函式的故事(1) 超詳!!!來進行了解\(^ ^)/
函式的嵌套呼叫和鏈式訪問
函式和函式之間可以有機的組合
函式的嵌套呼叫
函式可以嵌套呼叫,但是不能嵌套定義
例子1:
#include <stdio.h>
void print()
{
printf("禁止套娃!");
}
int test()
{
print();
return 0;
}
int main()
{
test();
return 0;
}
該例子就是通過在主函式呼叫了一個test函式,又在test函式里呼叫了一個print函式
函式的鏈式訪問
把一個函式的回傳值作為另外一個函式的引數
例子2:將arr2的字串拷貝到arr1,并列印arr1
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20]={0};
char arr2[]="Hello World";
//不用鏈式訪問
strcpy(arr1,arr2);
printf("%s\n",arr1);
//鏈式訪問
printf("%s\n",strcpy(arr1,arr2);
}
- 不用鏈式訪問,就是直接用strcpy函式將arr2的值傳給arr1后,再將arr1列印
- 用鏈式訪問就是將strcpy的回傳值作了printf的引數
所以我們要知道strcpy的回傳值是什么,可以通過上章講的網站,查詢,結果如下:
- Each of these functions returns the destination string. No return value is reserved to indicate an error
- 意思就是該函式回傳目標字串,沒有的話則指示錯誤
例子3:printf套娃列印(思考題哦)
#include <stdio.h>
int main()
{
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
- 最終列印的是434343嗎?還是43?或者還是其他值呢?
- 按照例2的解釋,我們第一步應該要清楚,這是函式的鏈式訪問
- 第二步,就要找到最內層函式的回傳值是是么
- 第三步就是將回傳值作為引數給到外層函式,開始套娃啦!
我們可以確定這是鏈式訪問,并且只有函式printf
通過資料找到printf的回傳值:
Each of these functions returns the number of characters printed, or a negative value if an error occurs
翻譯就是:這些函式中的每一個函式都回傳列印的字符數,如果發生錯誤,則回傳負值故最內層printf先列印43,而他的回傳值就是列印的字符數2,并且作為其外層printf的引數,中間的printf就列印2,而他則回傳1再作為最外層printf的引數,最后最外面的printf則列印1
得到最后的結果就是4321
函式的宣告和定義
函式的宣告
- 函式宣告的作用則是把函式的名字、函式型別以及形參型別、個數和順序通知編譯系統,以便在呼叫該函式時系統按此進行對照檢查
- 在書寫形式上,函式宣告可以把函式頭部復制過來,在后面加一個分號;而且在引數表中可以只寫各個引數的型別名,而不必寫引數名
- 函式的宣告一般出現在函式的使用之前,要滿足先宣告后使用
- 函式的宣告一般要放在頭檔案中的
函式的定義
- 函式定義是指對函式功能的確立,包括指定函式名,函式值型別、形參型別、函式體等,它是一個完整的、獨立的函式單位
個人理解
- 函式宣告其實是無關緊要的,如果我在呼叫函式之前,就定義了函式,就不需要函式宣告了
- 函式定義就像你發明了一個東西,但是你不告訴別人這是什么,別人也不會用,不知道,而函式宣告就是去告訴別人你這個發明的出現,
函式的遞回
什么是遞回
- 一個程序或函式在其定義或說明中有直接或間接呼叫自身的一種方法
- 遞回的主要思考方式:大事化小,小事化了
遞回的兩個必要條件
- 存在限制條件,當滿足這個限制條件的時候,遞回便不再繼續,
- 每次遞回呼叫之后越來越接近這個限制條件
例子5:最簡單的遞回(main函式)
#include <stdio.h>
int main()
{
printf("我會死回圈呀!\n");
main();
}
上面回圈大家可以通過除錯,得到一個報錯:Stack overflow,這就是堆疊溢位
至于為什么會出現堆疊溢位呢?大家可以發現,例5其實不滿足遞回的兩個必要條件,所以這個遞回是錯的,甚至不能稱為遞回
堆疊溢位
- 堆疊溢位就是緩沖區溢位的一種
- 緩沖區:程式在運行程序中,為了臨時存取資料的需要,一般都要分配一些記憶體空間,通常稱這些空間為緩沖區
- 如果向緩沖區中寫入超過其本身長度的資料,以致于緩沖區無法容納,就會造成緩沖區以外的存盤單元被改寫,這種現象就稱為緩沖區溢位
- 緩沖區長度一般與用戶自己定義的緩沖變數的型別有關
例子6:順序列印一個整型值(用遞回)
#include <stdio.h>
void print(int n)
{
if(n>9)
{
print(n/10);
}
printf("%d ",n%10);
}
int main()
{
int n;
scanf("%d",&n);
print(n);
return 0;
}
- 順序列印一個數,可以通過模10,除10,得到該數各個位上的數(最先得到最低位數字),然后通過陣列接收,逆序輸出就可以
- 而如果利用遞回,我們的思路就要變一下:假如我們print(123),我們其實就可以列印print(12)和printf(3);而print(12),其實就是print(1)和printf(2)最后print(1)我們就可以直接printf(1)
通過上面遞回的思路,化成代碼的思路就是:(例子6)
- 判斷這個數是不是個位數,即n<9,
- 若判斷的數字不小于9,則拆解成前面一部分和最后一位數,前面一部分先繼續判斷,判斷后,后面這個數再接著列印
- 若判斷的數字小于9,則直接列印
例子7:創建一個函式,模擬strlen,不能創建臨時變數
#include <stdio.h>
int my_strlen(char* str)
{
if (*str != '\0')
{
return 1 + my_strlen(str+1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = "hello World";
printf("%d", my_strlen(arr));
return 0;
}
- 這題如果能創建臨時變數,就可以定義一個count來計數,但是要求,所以可以思考一下遞回
遞回與迭代
- 迭代是重復反饋程序的活動,其目的通常是為了逼近所需目標或結果,每一次對程序的重復稱為一次“迭代”,而每一次迭代得到的結果會作為下一次迭代的初始值
例子7:求n的階乘
#include <stdio.h>
int fac(int n)
{
if(n<=1)
return 1;
else
return n*fac(n-1);
}
int main()
{
int n;
scanf("%d",&n);
int ret=fac(n);
printf("%d",ret);
return 0;
}
例7就是遞回與迭代的結合,當n>1時,會反復執行n*fac(n-1)這個陳述句;而n<=1時,就回傳1
例子8:求斐波那契數列
#include <stdio.h>
int fib(int n)
{
if(n<=2)
return 1;
else
return fib(n-1)+fib(n-2);
}
int main()
{
int n;
scanf("%d",&n);
printf("%d",fib(n));
return 0;
}
- 斐波那契數列第一個和第二個數為1,從第三個開始都等于前兩個數的和
- 所以只需要,判斷當n<=2時,回傳1,n>2時回傳前兩個數的和,即遞回和迭代的結合
注意:
- 當n較大時,如50,最終結果要等一段時間才能輸出,因為遞回的效率很低,比如fib(50)=fib(49)+fib(48),fib(49)=fib(48)+fib(47)…一個數后面會分散成很多分支,而且會出現很多已經重復出現的值,故要花時間大,效率低
- 當n很大時,如10000000,則最后程式可能會崩潰,因為每呼叫一次函式,都會在堆疊區開辟一份記憶體,而當n=10000000時,是需要開辟很多空間的,導致堆疊溢位
當n=50時,可作圖如下:

- 通過上圖可以知道,當n=50時,遞回呼叫函式需要很多次
- 并且中間會出現很多重復的值,需要再次計算
解決遞回效率問題
將遞回改寫成非遞回,
使用static物件替代nonstatic區域物件,在遞回函式設計中,可以使用static物件替代nonstatic區域物件(即堆疊物件),這不僅可以減少每次遞回呼叫和回傳時產生和釋放nonstatic物件的開銷, 而且static物件還可以保存遞回呼叫的中間狀態,并且可為各個呼叫層所訪問
例子9:斐波那契數列(非遞回,不考慮溢位)
#include <stdio.h>
int fib(int n)
{
int a=1;
int b=1;
int c=0;
while(n>2)
{
c=a+b;
a=b;
b=c;
n--;
}
return c;
}
int main()
{
int n;
scanf("%d",&n);
printf("%d",fib(n));
return 0;
}
遞回總結
- 許多問題是以遞回的形式進行解釋的,這只是因為它比非遞回的形式更為清晰,
- 但是這些問題的迭代實作往往比遞回實作效率更高,雖然代碼的可讀性稍微差些,
- 當一個問題相當復雜,難以用迭代實作時,此時遞回實作的簡潔性便可以補償它所帶來的運行時開 銷,
遞回思考題(后面章節會專門講述我寫的思路)
- 漢諾塔問題
- 青蛙跳臺階問題
遞回總結
- 許多問題是以遞回的形式進行解釋的,這只是因為它比非遞回的形式更為清晰,
- 但是這些問題的迭代實作往往比遞回實作效率更高,雖然代碼的可讀性稍微差些,
- 當一個問題相當復雜,難以用迭代實作時,此時遞回實作的簡潔性便可以補償它所帶來的運行時開銷,
最后函式的故事到此就告一段落了,但是這只是C世界中的一個故事而已,下一次,將會是陣列的故事,如果你喜歡聽的話,下一章,我們再見!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/279222.html
標籤:其他
