一篇文章帶你入門C語言:函數

函數

定義

程序裡的函數又被叫做子程序,他作為一個大型程序的部分代碼,有一或多個語句項組成。函數負責完成某項特定任務,提供瞭對過程的封裝和對細節的隱藏,這樣的代碼通常會被集成為軟件庫。

特點:

具備相對的獨立性一般有輸入值和返回值功能單一且靈活

函數的分類有:庫函數和自定義函數。

庫函數

定義

庫函數,顧名思義,放在庫裡供他人使用的函數。如打印輸出這樣的基礎功能,他不是業務性的代碼,在開發過程中使用率高且可移植性強,故C語言的基礎庫裡提供瞭這樣的一系列基礎功能的代碼。

一般庫函數有:

IO函數(input&output)—— printf scanf getchar putchar …字符串操作函數 —— strlen strcmp strcat strcpy …字符操作函數 —— tolower toupper …內存操作函數 —— memcpy menset memmove memcmp …時間/日期操作函數 —— time …數學函數 —— sqrt abs fabs pow …其他庫函數

介紹

為瞭掌握庫函數的使用方法的學習,我們可以參照權威網站 cplusplus 的解析為樣本,一般在不同的平臺上也是大同小異。一般都是按照這樣的順序對函數進行解析。

函數的基本信息功能描述函數參數返回值例子拓展

Example 1 strcpy

char * strcpy ( char * destination, const char * source);

庫函數解析示例

strcpy監視檢查尾0

當然這裡 strcpy 函數的返回值是目標空間的首地址,故接收是也可以使用函數的返回值。
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 );

memset函數解析

	char arr[20] = "damn it!";
	memset(arr, 'x', 2);
	//1.
	printf("%s\n", arr);
	//2.
	printf("%s\n", (char*)memset(arr, 'x', 2));

memset函數是以字節為單位,去修改我們的地址中的內容。

	int arr[30] = { 0 };
	memset(arr, 1, 5 * sizeof(int));

memset遇整型數組

這樣的話隻能把整型變量中每一個字節都變成1,而若想用此法置零則是可行的。

就按照這樣的方式去讀網站上對函數的解析內容。

註意

  • 每次使用庫函數都有引用#include頭文件

自定義函數

定義

庫函數雖好,但不可貪杯哦~ 庫函數雖多,但是畢竟不能實現所有功能,所以還是需要自定義函數來滿足我們的各種各樣的需求。自定義函數和庫函數一樣,有函數名、返回類型和函數參屬,但不同的是這些都由我們自己來設計。

形式

ret_type fun_name(para1,...)
{
    statment;//語句項
}
ret_type//返回類型
fun_name//函數名
para//參數

有瞭這樣的形式模板,我們就可以照葫蘆畫瓢瞭。

Example 1

找出兩個數的最大值

函數形式示例

如圖所示,寫函數,函數名、參數、返回類型都要對應。

Example 2 兩數交換

先看再程序設計中如何進行兩數交換,用醬油、醋和空瓶舉例。

兩數交換示例

先把a賦值給t,那麼現在t裡面存有a的值現在再把b賦值給a,這樣a還在t裡不會被覆蓋最後把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那個函數能夠實現這樣的功能呢?

Swap1僅僅是把a和b傳值給x和y,此時去修改x和y是影響不到a和b的。

Swap2是把a,b的地址傳給指針變量px和py,這樣的話,再函數內去將px和py解引用再修改,就可以指向a,b的內容瞭。簡而言之,通過指針指向實參的地址使得形參與實參同時發生變化。

傳值傳址示例

由圖可知,px和py內部存儲的是變量a和b的地址,這樣對px和py解引用就可以修改a和b的值。

由右圖的監視可看出,Swap1函數x和y確實發生瞭交換,但並沒有影響到a和b,Swap2函數的px、py和&a、&b是一個意思。

參數

函數參數分為實際參數和形式參數兩種,

實際參數又叫實參,實參可以是任意有確定值的形式,以便在進行函數調用時,將其傳給形參。形式參數又叫形參,隻有當函數調用時,他們才被分配確定值以及內存單元,調前不存在,調後銷毀,所以形參隻是形式上存在。

函數調用

1.傳值調用

形參實例化之後相當於實參的一份臨時拷貝,並且形參和實參占用不同的內存單元,本質上是兩個不同的變量,形參的修改不影響實參。

2.傳址調用

將外部變量的地址傳給函數參數,這樣的調用可使函數內外建立真正的聯系,即形參實參建立聯系。

練習

1.寫一個函數能夠判斷素數

#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;
}

函數的功能要單一且靈活,判斷素數就是判斷素數,打印的操作留給其他函數,這樣的話,寫出來的代碼才能夠很好的互相配合。

2.寫一個函數判斷一年是否為閏年

//是閏年返回1,不是閏年返回0
int is_leap_year(int y)
{
	return (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0));
}

不可以將兩個或者的條件分開成if…else…的形式,這樣的話1200,1600,2000這樣可整除400,也可整除100的數據就在第一輪判斷就淘汰瞭,進入不瞭第二個條件的判斷。

3.寫一個函數實現整型有序數組的二分查找

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]);
	//計算數組元素個數
	int ret = binary_search(arr, key, sz);//TDD - 測試驅動開發
	//找到返回下標0~9
	//找不到返回-1	
    if (ret == -1)
        printf("找不到\n");
	else
        printf("找到瞭,下標為%d", ret);
	return 0;
}

在主程序編寫代碼時,把binary_search函數當成庫函數一樣寫,並將函數的實現邏輯實現規定好,最後再去寫函數實現。

這樣的方法叫TDD(test drive develop)—測試驅動開發。

1.寫一個函數每調用一次,就將num的值加1

int Add(int num)
{
	num++;
}
int main()
{
	int num = 0;
	num = Add(num);
	return 0;
}

講到這裡基本內容就講完瞭,下面開始進一步的深入。

嵌套調用

函數可不可以嵌套定義?

當然是不可以的,函數與函數是平等的,是並列關系,不可以在任意函數(包括主函數)中定義其他函數。

但是函數是可以互相調用的。

void fun1()
{
    printf("hanpidiaoyong\n");
}
void fun2()
{
    fun1();
}
int main()
{
    fun2();
    return 0;
}

如代碼所示,main函數調用fun2函數,fun2函數又調用fun1函數,最終在屏幕上打印憨批調用字樣/[doge]。

鏈式訪問

鏈式訪問(chain access),顧名思義,把一個函數的返回值作為另一個函數的參數。像是用鏈子把函數首尾相連拴起來。

如:

int main()
{
    printf("%d\n",strlen("abcde")); //把strlen的返回值作為printf的參數   
    return 0;
}
int main()
{
	char arr1[20] = "xxxxxxx";
	char arr2[20] = "abcde";
	//strcpy(arr1,arr2);
	printf("%s\n", strcpy(arr1, arr2));//strcpy函數的返回值是目標空間首元素地址
	return 0;
}

Example 1

如果覺得掌握瞭的話,可以看看這個經典例子。

	printf("%d", printf("%d", printf("%d", 43)));

請問這條語句輸出什麼?

想要知道這個,那必然要先瞭解 printf 函數的返回值是什麼,通過MSDN或者cplusplus.com網站去查找。

可以看到 printf 函數的返回值是打印字符的個數,如果發生錯誤,則返回負值。(ps:scanf的返回值是輸出字符的個數)

首先可以看出第三個printf打印瞭43;

然後第二個printf打印瞭第三個printf的返回值為2;

最後第一個printf打印第二個printf的返回值1;

所以屏幕上打印瞭4321。

筆者良心說一句,如果學校裡有人說自己C語言不錯,那麼請拿這題考考他。

函數聲明

代碼是從前往後執行的,如果函數定義在後面的話,調用時便會發出警告:函數未定義,若想消除警告,我們便需要在前面聲明一下。

void test();
int main(){
	test();
}
void test()
{}        

定義:聲明就是把函數定義在加個;,目的是告訴編譯器函數的返回類型、函數名、參數這些具體信息。

特點

函數的聲明一般出現在函數使用之前。

函數的聲明一般放在頭文件中。

在工作的時候,一般是把函數的聲明、定義和使用放在三個不同的文件內,方便所有人協作。如:

函數跨文件示例

鏈接:兩個.c的源文件編譯之後,會分別生成.obj的目標文件,然後再鏈接起來,最後生成.exe的可執行文件。

在C語言,不提供源碼,也可以使用文件的內容,怎麼做的呢?

請移步至我的其他博客:關於vs2019的各種使用問題及解決方法(隨即更新)

頭文件引用

#include的預編譯指令是在預編譯階段將頭文件內的所有內容拷貝到源文件內。

所以,頭文件中的內容若是內容重復包含則會造成效率降低。那麼,怎麼解決這件事呢?

  • 頭文件中包含語句#pragma once使得頭文件中不會重復包含其他頭文件;
  • 添加這樣的代碼,將語句包含起來。
#ifndef __ADD_H__// if not define 
#define __ADD_H__//define
//Add函數聲明
extern int Add(int x, int y);
#endif//end if

早期都是用第二種方法的,這兩種方法是完全等價的。

函數遞歸

什麼叫函數遞歸呢?

程序自身調用自身的編程技巧叫遞歸。

特點

大型復雜問題層層轉化為小規模的問題少量程序描述除多次運算

遞歸的思維方法在於:大事化小。

Example 1

接收一個無符號整型值,按照順序打印其每一位。如輸入:1234,輸出:1 2 3 4 .

那我們創建一個函數叫print,若print(1234),則剝離一位變成print(123)+4,再剝離一位成print(12)+3+4,再來一位就是print(1)+2+3+4,最後隻有一位瞭,那就全部用printf 函數打印。如:

函數調用解析

我們發現隻要將數字1234模10就可以得到4,除10便可以得到123,如此模10除10循環往復,可以將1 2 3 4全部剝離出來。

//函數遞歸
void print(size_t n)
{
	if (n > 9)//隻有1位便不再往下進行
	{
		print(n / 10);
	}
	printf("%d ", n%10);
}

具體流程可參考下面這張圖。

函數遞歸調用示例

紅線部分即在返回的時候,n是本次函數n,而不是前一次調用的n。

遞歸遞歸,就是遞推加回歸。

現在有兩個問題

  • if(n > 9)這個條件沒有行不行?沒有會怎麼樣?

自然是不行的,我們在上面的推到中發現,最後1<9條件不成立,就結束瞭遞歸,否則會永遠遞歸下去,造成死循環且耗幹瞭棧區。

  • 或者是我們不論代碼的正確性,將print(n / 10)改成print(n)會怎麼樣?

改成print(n)的話每次遞歸都是相同的值,遞歸也會無止境的延續下去。

這樣便引出瞭我們遞歸的兩個重要的必要條件:

必要條件

  • 必須存在限制條件,滿足條件時,遞歸不在繼續
  • 每次遞歸調用後必須越來越接近限制條件 函數棧幀

在第一個問題中,如果我們要去試驗的話,編譯器會報出這樣的錯誤:

遞歸棧溢出示例

Stackoverflow(棧溢出)。

內存劃分

內存粗略的劃分為棧區,堆區,靜態區。

棧區主要存放:局部變量,形參(形參和局部變量差不多)動態內存分配:malloc calloc等函數開辟空間靜態區主要存放:全局變量,static修飾的靜態變量

若是把棧區放大細看的話,如圖所示,有為main函數開辟的空間和print函數開辟的空間,為函數開辟的空間叫函數棧幀也可以叫運行時堆棧。

程序開始執行時開辟空間,程序結束時銷毀空間函數每調用一次就在棧上開辟一次空間,遞歸返回時,空間會被回收

Example 2

不創建臨時變量,實現Strlen函數

int my_strlen1(char* pa)
{
    int count = 0;
	while (*pa++ != '\0')
	{//pa++;
		count++;
	}
	return count;
}
//my_strlen求字符串長度
int my_strlen(char* pa)
{
	if (*pa == 0){
		return 0;
	}
	return 1+my_strlen(pa + 1);//直接返回長度	
}

具體的思考方式呢,就是隻要第一個字符不是0,那我們就在外面+1並且跳到下一個字符,直到找到‘\0’,那麼我們返回0。

ps:

字符指針+1,向後跳一個字節
整型指針+1,向後跳四個字節
指針+1都是向後跳一個元素的地址,指針類型不同向後跳的字節也不同

my_strlen函數思考方法示例

my_strlen求字符串長度函數解析

函數迭代

遞歸、迭代的區別?

遞歸是重復調用函數自身實現循環。

迭代是函數內某段代碼實現循環,循環代碼中變量既參與運算同時也保存結果,當前保存的結果作為下一次循環計算的初始值。

遞歸循環中,遇到滿足終止條件的情況時逐層返回來結束。

迭代則使用計數器結束循環。

當然很多情況都是多種循環混合采用,這要根據具體需求。

Example 3

求n的階乘

int fac(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * fac(n - 1);
}

Example 4

求第n個斐波那契數

int fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}

但是這個方法效率是非常低的,當數字特別大時,層層拆分下來,時間效率是 O ( 2 n ) O(2^n) O(2n)。

根據公式可知,第三個斐波那契數可由前兩個得到,我們利用這個規律

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;
}

上一個c變成瞭b,上一個b變成瞭a。如此循環往復。

利用迭代的方式,計算一個數隻需要計算n-2次,這樣的話時間復雜度就是 O ( n ) O(n) O(n)。效率大大提高。

有這兩題我們可以發現,什麼時候用遞歸簡單呢?

1.有公式有模板的時候
2.遞歸簡單,非遞歸復雜的時候
3.有明顯問題的時候

學有餘力的話,還可以考慮實現倆個經典題目

1.漢諾塔問題
2.青蛙跳臺階問題

總結

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: