一篇文章帶你入門C語言:函數(shù)
函數(shù)
定義
程序里的函數(shù)又被叫做子程序,他作為一個(gè)大型程序的部分代碼,有一或多個(gè)語句項(xiàng)組成。函數(shù)負(fù)責(zé)完成某項(xiàng)特定任務(wù),提供了對(duì)過程的封裝和對(duì)細(xì)節(jié)的隱藏,這樣的代碼通常會(huì)被集成為軟件庫。
特點(diǎn):
具備相對(duì)的獨(dú)立性一般有輸入值和返回值功能單一且靈活
函數(shù)的分類有:庫函數(shù)和自定義函數(shù)。
庫函數(shù)
定義
庫函數(shù),顧名思義,放在庫里供他人使用的函數(shù)。如打印輸出這樣的基礎(chǔ)功能,他不是業(yè)務(wù)性的代碼,在開發(fā)過程中使用率高且可移植性強(qiáng),故C語言的基礎(chǔ)庫里提供了這樣的一系列基礎(chǔ)功能的代碼。
一般庫函數(shù)有:
IO函數(shù)(input&output)—— printf scanf getchar putchar …字符串操作函數(shù) —— strlen strcmp strcat strcpy …字符操作函數(shù) —— tolower toupper …內(nèi)存操作函數(shù) —— memcpy menset memmove memcmp …時(shí)間/日期操作函數(shù) —— time …數(shù)學(xué)函數(shù) —— sqrt abs fabs pow …其他庫函數(shù)
介紹
為了掌握庫函數(shù)的使用方法的學(xué)習(xí),我們可以參照權(quán)威網(wǎng)站 cplusplus 的解析為樣本,一般在不同的平臺(tái)上也是大同小異。一般都是按照這樣的順序?qū)瘮?shù)進(jìn)行解析。
函數(shù)的基本信息功能描述函數(shù)參數(shù)返回值例子拓展
Example 1 strcpy
char * strcpy ( char * destination, const char * source);
當(dāng)然這里 strcpy 函數(shù)的返回值是目標(biāo)空間的首地址,故接收是也可以使用函數(shù)的返回值。 char arr1[20] = { 0 }; char arr2[] = "damn it!"; //1. char* ret = strcpy(arr1, arr2); printf("%s\n", ret); //2. printf("%s\n",strcpy(arr1, arr2));
Example 2 memset
void * ( void * ptr, int value, size_t num );
char arr[20] = "damn it!"; memset(arr, 'x', 2); //1. printf("%s\n", arr); //2. printf("%s\n", (char*)memset(arr, 'x', 2));
memset函數(shù)是以字節(jié)為單位,去修改我們的地址中的內(nèi)容。
int arr[30] = { 0 }; memset(arr, 1, 5 * sizeof(int));
這樣的話只能把整型變量中每一個(gè)字節(jié)都變成1,而若想用此法置零則是可行的。
就按照這樣的方式去讀網(wǎng)站上對(duì)函數(shù)的解析內(nèi)容。
注意
- 每次使用庫函數(shù)都有引用#include頭文件
自定義函數(shù)
定義
庫函數(shù)雖好,但不可貪杯哦~ 庫函數(shù)雖多,但是畢竟不能實(shí)現(xiàn)所有功能,所以還是需要自定義函數(shù)來滿足我們的各種各樣的需求。自定義函數(shù)和庫函數(shù)一樣,有函數(shù)名、返回類型和函數(shù)參屬,但不同的是這些都由我們自己來設(shè)計(jì)。
形式
ret_type fun_name(para1,...) { statment;//語句項(xiàng) } ret_type//返回類型 fun_name//函數(shù)名 para//參數(shù)
有了這樣的形式模板,我們就可以照葫蘆畫瓢了。
Example 1
找出兩個(gè)數(shù)的最大值
如圖所示,寫函數(shù),函數(shù)名、參數(shù)、返回類型都要對(duì)應(yīng)。
Example 2 兩數(shù)交換
先看再程序設(shè)計(jì)中如何進(jìn)行兩數(shù)交換,用醬油、醋和空瓶舉例。
先把a(bǔ)賦值給t,那么現(xiàn)在t里面存有a的值現(xiàn)在再把b賦值給a,這樣a還在t里不會(huì)被覆蓋最后把t(里的a)賦值給b,這樣就完成了a和b的互換。
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那個(gè)函數(shù)能夠?qū)崿F(xiàn)這樣的功能呢?
Swap1僅僅是把a(bǔ)和b傳值給x和y,此時(shí)去修改x和y是影響不到a和b的。
Swap2是把a(bǔ),b的地址傳給指針變量px和py,這樣的話,再函數(shù)內(nèi)去將px和py解引用再修改,就可以指向a,b的內(nèi)容了。簡(jiǎn)而言之,通過指針指向?qū)崊⒌牡刂肥沟眯螀⑴c實(shí)參同時(shí)發(fā)生變化。
由圖可知,px和py內(nèi)部存儲(chǔ)的是變量a和b的地址,這樣對(duì)px和py解引用就可以修改a和b的值。
由右圖的監(jiān)視可看出,Swap1函數(shù)x和y確實(shí)發(fā)生了交換,但并沒有影響到a和b,Swap2函數(shù)的px、py和&a、&b是一個(gè)意思。
參數(shù)
函數(shù)參數(shù)分為實(shí)際參數(shù)和形式參數(shù)兩種,
實(shí)際參數(shù)又叫實(shí)參,實(shí)參可以是任意有確定值的形式,以便在進(jìn)行函數(shù)調(diào)用時(shí),將其傳給形參。形式參數(shù)又叫形參,只有當(dāng)函數(shù)調(diào)用時(shí),他們才被分配確定值以及內(nèi)存單元,調(diào)前不存在,調(diào)后銷毀,所以形參只是形式上存在。
函數(shù)調(diào)用
1.傳值調(diào)用
形參實(shí)例化之后相當(dāng)于實(shí)參的一份臨時(shí)拷貝,并且形參和實(shí)參占用不同的內(nèi)存單元,本質(zhì)上是兩個(gè)不同的變量,形參的修改不影響實(shí)參。
2.傳址調(diào)用
將外部變量的地址傳給函數(shù)參數(shù),這樣的調(diào)用可使函數(shù)內(nèi)外建立真正的聯(lián)系,即形參實(shí)參建立聯(lián)系。
練習(xí)
1.寫一個(gè)函數(shù)能夠判斷素?cái)?shù)
#include <math.h> int is_prime(int n) { //試除法 int j = 0; for (j = 2; j <= sqrt(n); j++) { if (n % j == 0) return 0; } return 1; }
函數(shù)的功能要單一且靈活,判斷素?cái)?shù)就是判斷素?cái)?shù),打印的操作留給其他函數(shù),這樣的話,寫出來的代碼才能夠很好的互相配合。
2.寫一個(gè)函數(shù)判斷一年是否為閏年
//是閏年返回1,不是閏年返回0 int is_leap_year(int y) { return (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0)); }
不可以將兩個(gè)或者的條件分開成if…else…的形式,這樣的話1200,1600,2000這樣可整除400,也可整除100的數(shù)據(jù)就在第一輪判斷就淘汰了,進(jìn)入不了第二個(gè)條件的判斷。
3.寫一個(gè)函數(shù)實(shí)現(xiàn)整型有序數(shù)組的二分查找
int binary_search(char arr[], int k, int sz) { int left = 0; int right = sz - 1; while (left <= right) { int mid = (left+right)/2; if (arr[mid] > k) { right = mid - 1; } else if (arr[mid] < k) { left = mid + 1; } else return mid; } return -1; }
而在主程序中是這樣的
int main() { char arr[20] = { 1,2,3,4,5,6,7,8,9,10 }; int key = 7; int sz = sizeof(arr) / sizeof(arr[0]); //計(jì)算數(shù)組元素個(gè)數(shù) int ret = binary_search(arr, key, sz);//TDD - 測(cè)試驅(qū)動(dòng)開發(fā) //找到返回下標(biāo)0~9 //找不到返回-1 if (ret == -1) printf("找不到\n"); else printf("找到了,下標(biāo)為%d", ret); return 0; }
在主程序編寫代碼時(shí),把binary_search函數(shù)當(dāng)成庫函數(shù)一樣寫,并將函數(shù)的實(shí)現(xiàn)邏輯實(shí)現(xiàn)規(guī)定好,最后再去寫函數(shù)實(shí)現(xiàn)。
這樣的方法叫TDD(test drive develop)—測(cè)試驅(qū)動(dòng)開發(fā)。
1.寫一個(gè)函數(shù)每調(diào)用一次,就將num的值加1
int Add(int num) { num++; } int main() { int num = 0; num = Add(num); return 0; }
講到這里基本內(nèi)容就講完了,下面開始進(jìn)一步的深入。
嵌套調(diào)用
函數(shù)可不可以嵌套定義?
當(dāng)然是不可以的,函數(shù)與函數(shù)是平等的,是并列關(guān)系,不可以在任意函數(shù)(包括主函數(shù))中定義其他函數(shù)。
但是函數(shù)是可以互相調(diào)用的。
void fun1() { printf("hanpidiaoyong\n"); } void fun2() { fun1(); } int main() { fun2(); return 0; }
如代碼所示,main函數(shù)調(diào)用fun2函數(shù),fun2函數(shù)又調(diào)用fun1函數(shù),最終在屏幕上打印憨批調(diào)用字樣/[doge]。
鏈?zhǔn)皆L問
鏈?zhǔn)皆L問(chain access),顧名思義,把一個(gè)函數(shù)的返回值作為另一個(gè)函數(shù)的參數(shù)。像是用鏈子把函數(shù)首尾相連拴起來。
如:
int main() { printf("%d\n",strlen("abcde")); //把strlen的返回值作為printf的參數(shù) return 0; }
int main() { char arr1[20] = "xxxxxxx"; char arr2[20] = "abcde"; //strcpy(arr1,arr2); printf("%s\n", strcpy(arr1, arr2));//strcpy函數(shù)的返回值是目標(biāo)空間首元素地址 return 0; }
Example 1
如果覺得掌握了的話,可以看看這個(gè)經(jīng)典例子。
printf("%d", printf("%d", printf("%d", 43)));
請(qǐng)問這條語句輸出什么?
想要知道這個(gè),那必然要先了解 printf 函數(shù)的返回值是什么,通過MSDN或者cplusplus.com網(wǎng)站去查找。
可以看到 printf 函數(shù)的返回值是打印字符的個(gè)數(shù),如果發(fā)生錯(cuò)誤,則返回負(fù)值。(ps:scanf的返回值是輸出字符的個(gè)數(shù))
首先可以看出第三個(gè)printf打印了43;
然后第二個(gè)printf打印了第三個(gè)printf的返回值為2;
最后第一個(gè)printf打印第二個(gè)printf的返回值1;
所以屏幕上打印了4321。
筆者良心說一句,如果學(xué)校里有人說自己C語言不錯(cuò),那么請(qǐng)拿這題考考他。
函數(shù)聲明
代碼是從前往后執(zhí)行的,如果函數(shù)定義在后面的話,調(diào)用時(shí)便會(huì)發(fā)出警告:函數(shù)未定義,若想消除警告,我們便需要在前面聲明一下。
void test(); int main(){ test(); } void test() {}
定義:聲明就是把函數(shù)定義在加個(gè);,目的是告訴編譯器函數(shù)的返回類型、函數(shù)名、參數(shù)這些具體信息。
特點(diǎn)
函數(shù)的聲明一般出現(xiàn)在函數(shù)使用之前。
函數(shù)的聲明一般放在頭文件中。
在工作的時(shí)候,一般是把函數(shù)的聲明、定義和使用放在三個(gè)不同的文件內(nèi),方便所有人協(xié)作。如:
鏈接:兩個(gè).c的源文件編譯之后,會(huì)分別生成.obj的目標(biāo)文件,然后再鏈接起來,最后生成.exe的可執(zhí)行文件。
在C語言,不提供源碼,也可以使用文件的內(nèi)容,怎么做的呢?
請(qǐng)移步至我的其他博客:關(guān)于vs2019的各種使用問題及解決方法(隨即更新)
頭文件引用
#include的預(yù)編譯指令是在預(yù)編譯階段將頭文件內(nèi)的所有內(nèi)容拷貝到源文件內(nèi)。
所以,頭文件中的內(nèi)容若是內(nèi)容重復(fù)包含則會(huì)造成效率降低。那么,怎么解決這件事呢?
- 頭文件中包含語句#pragma once使得頭文件中不會(huì)重復(fù)包含其他頭文件;
- 添加這樣的代碼,將語句包含起來。
#ifndef __ADD_H__// if not define #define __ADD_H__//define //Add函數(shù)聲明 extern int Add(int x, int y); #endif//end if
早期都是用第二種方法的,這兩種方法是完全等價(jià)的。
函數(shù)遞歸
什么叫函數(shù)遞歸呢?
程序自身調(diào)用自身的編程技巧叫遞歸。
特點(diǎn)
大型復(fù)雜問題層層轉(zhuǎn)化為小規(guī)模的問題少量程序描述除多次運(yùn)算
遞歸的思維方法在于:大事化小。
Example 1
接收一個(gè)無符號(hào)整型值,按照順序打印其每一位。如輸入:1234,輸出:1 2 3 4 .
那我們創(chuàng)建一個(gè)函數(shù)叫print,若print(1234),則剝離一位變成print(123)+4,再剝離一位成print(12)+3+4,再來一位就是print(1)+2+3+4,最后只有一位了,那就全部用printf 函數(shù)打印。如:
我們發(fā)現(xiàn)只要將數(shù)字1234模10就可以得到4,除10便可以得到123,如此模10除10循環(huán)往復(fù),可以將1 2 3 4全部剝離出來。
//函數(shù)遞歸 void print(size_t n) { if (n > 9)//只有1位便不再往下進(jìn)行 { print(n / 10); } printf("%d ", n%10); }
具體流程可參考下面這張圖。
紅線部分即在返回的時(shí)候,n是本次函數(shù)n,而不是前一次調(diào)用的n。
遞歸遞歸,就是遞推加回歸。
現(xiàn)在有兩個(gè)問題
- if(n > 9)這個(gè)條件沒有行不行?沒有會(huì)怎么樣?
自然是不行的,我們?cè)谏厦娴耐频街邪l(fā)現(xiàn),最后1<9條件不成立,就結(jié)束了遞歸,否則會(huì)永遠(yuǎn)遞歸下去,造成死循環(huán)且耗干了棧區(qū)。
- 或者是我們不論代碼的正確性,將print(n / 10)改成print(n)會(huì)怎么樣?
改成print(n)的話每次遞歸都是相同的值,遞歸也會(huì)無止境的延續(xù)下去。
這樣便引出了我們遞歸的兩個(gè)重要的必要條件:
必要條件
- 必須存在限制條件,滿足條件時(shí),遞歸不在繼續(xù)
- 每次遞歸調(diào)用后必須越來越接近限制條件 函數(shù)棧幀
在第一個(gè)問題中,如果我們要去試驗(yàn)的話,編譯器會(huì)報(bào)出這樣的錯(cuò)誤:
Stackoverflow
(棧溢出)。
內(nèi)存粗略的劃分為棧區(qū),堆區(qū),靜態(tài)區(qū)。
棧區(qū)主要存放:局部變量,形參(形參和局部變量差不多)動(dòng)態(tài)內(nèi)存分配:malloc calloc等函數(shù)開辟空間靜態(tài)區(qū)主要存放:全局變量,static修飾的靜態(tài)變量
若是把棧區(qū)放大細(xì)看的話,如圖所示,有為main函數(shù)開辟的空間和print函數(shù)開辟的空間,為函數(shù)開辟的空間叫函數(shù)棧幀也可以叫運(yùn)行時(shí)堆棧。
程序開始執(zhí)行時(shí)開辟空間,程序結(jié)束時(shí)銷毀空間函數(shù)每調(diào)用一次就在棧上開辟一次空間,遞歸返回時(shí),空間會(huì)被回收
Example 2
不創(chuàng)建臨時(shí)變量,實(shí)現(xiàn)Strlen函數(shù)
int my_strlen1(char* pa) { int count = 0; while (*pa++ != '\0') {//pa++; count++; } return count; }
//my_strlen求字符串長(zhǎng)度 int my_strlen(char* pa) { if (*pa == 0){ return 0; } return 1+my_strlen(pa + 1);//直接返回長(zhǎng)度 }
具體的思考方式呢,就是只要第一個(gè)字符不是0,那我們就在外面+1并且跳到下一個(gè)字符,直到找到‘\0',那么我們返回0。
ps:
字符指針+1,向后跳一個(gè)字節(jié)
整型指針+1,向后跳四個(gè)字節(jié)
指針+1都是向后跳一個(gè)元素的地址,指針類型不同向后跳的字節(jié)也不同
函數(shù)迭代
遞歸、迭代的區(qū)別?
遞歸是重復(fù)調(diào)用函數(shù)自身實(shí)現(xiàn)循環(huán)。
迭代是函數(shù)內(nèi)某段代碼實(shí)現(xiàn)循環(huán),循環(huán)代碼中變量既參與運(yùn)算同時(shí)也保存結(jié)果,當(dāng)前保存的結(jié)果作為下一次循環(huán)計(jì)算的初始值。
遞歸循環(huán)中,遇到滿足終止條件的情況時(shí)逐層返回來結(jié)束。
迭代則使用計(jì)數(shù)器結(jié)束循環(huán)。
當(dāng)然很多情況都是多種循環(huán)混合采用,這要根據(jù)具體需求。
Example 3
求n的階乘
int fac(int n) { if (n <= 1) return 1; else return n * fac(n - 1); }
Example 4
求第n個(gè)斐波那契數(shù)
int fib(int n) { if (n <= 2) return 1; else return fib(n - 1) + fib(n - 2); }
但是這個(gè)方法效率是非常低的,當(dāng)數(shù)字特別大時(shí),層層拆分下來,時(shí)間效率是 O ( 2 n ) O(2^n) O(2n)。
根據(jù)公式可知,第三個(gè)斐波那契數(shù)可由前兩個(gè)得到,我們利用這個(gè)規(guī)律
int fib(int n) { int a = 1; int b = 1; int c = 1; while (n >= 3) { c = a + b; a = b; b = c; n--; } return c; }
上一個(gè)c變成了b,上一個(gè)b變成了a。如此循環(huán)往復(fù)。
利用迭代的方式,計(jì)算一個(gè)數(shù)只需要計(jì)算n-2次,這樣的話時(shí)間復(fù)雜度就是 O ( n ) O(n) O(n)。效率大大提高。
有這兩題我們可以發(fā)現(xiàn),什么時(shí)候用遞歸簡(jiǎn)單呢?
1.有公式有模板的時(shí)候
2.遞歸簡(jiǎn)單,非遞歸復(fù)雜的時(shí)候
3.有明顯問題的時(shí)候
學(xué)有余力的話,還可以考慮實(shí)現(xiàn)倆個(gè)經(jīng)典題目
1.漢諾塔問題
2.青蛙跳臺(tái)階問題
總結(jié)
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C++通過類實(shí)現(xiàn)控制臺(tái)貪吃蛇
這篇文章主要為大家詳細(xì)介紹了C++通過類實(shí)現(xiàn)控制臺(tái)貪吃蛇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04詳解如何將Spire.PDF for C++集成到C++程序中
Spire.PDF for C++ 是一個(gè)專業(yè)的 PDF 庫,供開發(fā)人員在任何類型的 C++ 應(yīng)用程序中閱讀、創(chuàng)建、編輯和轉(zhuǎn)換 PDF 文檔,本文主要介紹了兩種不同的方式將 Spire.PDF for C++ 集成到您的 C++ 應(yīng)用程序中,希望對(duì)大家有所幫助2023-11-11