天天看點

第3章 函數1. 函數是什麼?2. C語言中函數的分類3. 函數的參數4. 函數的調用5. 函數的嵌套調用和鍊式通路6. 函數的聲明和調用7. 遞歸

目錄

  • 1. 函數是什麼?
  • 2. C語言中函數的分類
    • 2.1 庫函數
    • 2.2 自定義函數
  • 3. 函數的參數
  • 4. 函數的調用
  • 5. 函數的嵌套調用和鍊式通路
    • 5.1 嵌套調用
    • 5.2 鍊式通路
  • 6. 函數的聲明和調用
  • 7. 遞歸
    • 7.1 什麼是遞歸?
    • 7.2 遞歸的兩個必要條件
    • 7.3 遞歸與疊代

1. 函數是什麼?

維基百科中對函數的定義:子程式

  • 在計算機科學中,子程式(英語:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一個大型程式中的某部分代碼, 由一個或多個語句塊組成。它負責完成某項特定任務,而且相較于其他代碼,具備相對的獨立性。
  • 一般會有輸入參數并有傳回值,提供對過程的封裝和細節的隐藏。這些代碼通常被內建為軟體庫。

2. C語言中函數的分類

  • 庫函數
  • 自定義函數

C語言标準規定了一些庫函數:

// 函數名,參數類型,傳回值類型,函數功能規定好。

編譯器的廠商提供庫函數。

2.1 庫函數

  • C語言本身提供的函數 – 稱為 庫函數。
  • C語言常見的庫函數有:
    • IO函數(input ,output,輸入輸出函數)
    • 字元串操作函數
    • 字元操作函數
    • 記憶體操作函數
    • 時間/日期函數
    • 數學函數
    • 其他庫函數

參考文檔,學習幾個庫函數。

例一:strcpy – string copy – 字元串複制有關。

#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "bit";
	char arr2[] = "#######";
	strcpy(arr2, arr1);
	printf("%s\n", arr2);

	// strlen  -- string length  -- 跟字元串長度有關的
	// strcpy  -- string copy    -- 字元串複制有關。// /0也會拷貝

	return 0;
}

// 運作結果
// bit
           
第3章 函數1. 函數是什麼?2. C語言中函數的分類3. 函數的參數4. 函數的調用5. 函數的嵌套調用和鍊式通路6. 函數的聲明和調用7. 遞歸

例二:memset – memory – 記憶體 set – 設定

#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] = "almost every programmer should know memset!";
  memset (str,'-',6);
  puts (str);
  return 0;
}
// 運作結果
// ------ every programmer should know memset!


#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "hello bit";
	char* ret = (char*)memset(arr, 'x', 5);
	printf("%s\n", ret);
	return 0;
}
           

需要學會查詢工具的使用:

MSDN(Microsoft Developer Network)

www.cplusplus.com

http://en.cppreference.com (英文版)

http://zh.cppreference.com (中文版)

2.2 自定義函數

函數的設計應追求高内聚低耦合。

// 文法結構
ret_type fun_name(para1, * )
{
 	statement;//語句項
}

// ret_type 傳回類型
// fun_name 函數名
// para1    函數參數
           

舉例:

求兩個整數中的最大值。
#include <stdio.h>

// 定義宏
#define MAX(X, Y) (X>Y?X:Y)

// 定義函數
int get_max(int a, int b) 
{
	return a > b ? a : b;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("get_max(a, b) = %d\n", get_max(a, b));

	printf("MAX = %d", MAX(a, b));
	return 0;
}
           
寫一個函數,可以交換兩個整型變量的值。
#include <stdio.h>

//void Swap1(int a, int b)
//{
//	int tmp = 0;
//	tmp = a;
//	a = b;
//	b = tmp;
//}

// 不能完成任務
// 當實參傳給形參的時候,
// 形參是實參的一份臨時拷貝
// 對形參的修改是不會改變實參的

void Swap2(int* pa, int* pb) {
	int tmp = 0;
	tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
int main()
{
	int num1 = 10;
	int num2 = 20;

    // Swap1(num1, num2);
	// printf("num1 = %d, num2 = %d\n", num1, num2);
	
	Swap2(&num1, &num2);
	printf("num1 = %d, num2 = %d\n", num1, num2);
	return 0;
}
// 運作結果
// num1 = 10, num2 = 20
// num1 = 20, num2 = 10
           

總結:

能将函數處理結果的兩個資料傳回給主調函數,的方法有:

  1. 形參用數組;
  2. 形參用兩個指針;
  3. 用兩個全局變量。

3. 函數的參數

實際參數(實參):

真實傳給函數的參數,叫實參。

實參可以是:常量、變量、表達式、函數等。

無論實參是何種類型的量,在進行函數調用時,它們都必須有确定的值,以便把這些值傳送給形參。

形式參數(形參):

形式參數是指函數名後括号中的變量,因為形式參數隻有在函數被調用的過程中才執行個體化(配置設定 記憶體單元),是以叫形式參數。形式參數當函數調用完成之後就自動銷毀了。是以形式參數隻在函數中有效。

上面例子中

Swap1

Swap2

函數中的參數

a

b

pa

pb

都是 形式參數。在

main

函數中傳給

Swap1

函數的

num1

num2

和傳給

Swap2

函數的

&num1

&num2

是 實際參數。

這裡我們對函數的實參和形參進行分析:

第3章 函數1. 函數是什麼?2. C語言中函數的分類3. 函數的參數4. 函數的調用5. 函數的嵌套調用和鍊式通路6. 函數的聲明和調用7. 遞歸

可以看到

Swap1

函數在調用的時候,

a

b

擁有自己的空間,同時擁有了和實參一模一樣的内容。是以我們可以簡單的認為:形參執行個體化之後其實相當于實參的一份臨時拷貝。

4. 函數的調用

傳值調用:

函數的形參和實參分别有不同的記憶體塊,對形參的修改不會影響實參。

形參是實參的一份 臨時拷貝。

傳址調用:

  • 傳址調用是把函數外部建立變量的記憶體位址傳遞給函數參數的一種調用函數的格式。
  • 這種傳參方式可以讓函數和函數外邊的變量建立起真正的聯系,也就是函數内部可以直接操作函數外部的變量。

練習:

寫一個函數,判斷一個數是否是素數。
#include <stdio.h>
#include <math.h>

int is_prime(int x)
{
	int i = 0;
	for (i = 2; i <= sqrt(x); i++)
		if (x % i == 0)
			return 0;
	return 1;
}

int main()
{
	int a = 18;
	if (1 == is_prime(a))
		printf("%d是素數\n", a);
	else
		printf("%d不是素數\n", a);

	return 0;
}
           
寫一個函數,判斷一年是否是閏年。
#include <stdio.h>
int is_leap_year(int x)
{
	return ((x % 4 == 0 && x % 100 != 0) || (x % 400 == 0));
}
int main()
{
	int year = 2008;
	if(1 == is_leap_year(year))
		printf("%d是閏年\n", year);
	else
		printf("%d不是閏年\n", year);
	return 0;
}
           
寫一個函數,實作一個整形數組的二分查找。
#include <stdio.h>
				// 本質上這裡的 a 是一個指針
int binary_search(int a[], int b,int sz)
{
	int left = 0;
	// 調用函數的時候不能使用這種方式,求數組長度。
	// 	   sizeof() 計算指針的大小就是 4 或者 8
	// int sz = sizeof(a) / sizeof(a[0]);
	 int right = sz - 1;

	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (a[mid] < b)
			left = mid + 1;
		else if (a[mid] > b)
			right = mid - 1;
		else
			return mid;
	}
	return -1;
}

int main()
{
	// 二分查找
	// 在一個有序數組中查找具體的某個數
	// 如果找到了,傳回這個數的下标,找不到傳回-1
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 1;
	int sz = sizeof(arr) / sizeof(arr[0]);
	// 傳遞過去的是首元素的位址
	int ret = binary_search(arr,k,sz);
	if (ret == -1)
		printf("找不到指定數字\n");
	else
		printf("找到了,下标是:%d\n", ret);

	return 0;
}
           
寫一個函數,每調用一次這個函數,就将num的值增加1。
#include <stdio.h>
void Add(int* p)
{
	(*p)++;
    // 由于++的優先級比*高,如果不給*p加括号,那麼将得不出正确的結果。
}
int main()
{
	int num = 0;
	Add(&num);
	printf("num = %d\n", num);
	Add(&num);
	printf("num = %d\n", num);
	Add(&num);
	printf("num = %d\n", num);

	return 0;
}
// num = 1
// num = 2
// num = 3
           

5. 函數的嵌套調用和鍊式通路

5.1 嵌套調用

函數可以嵌套調用,但不能嵌套定義。
#include <stdio.h>

void new_line()
{
	printf("hello world\n");
}

void three_line()
{
	int i = 0;
	for(i=0; i<3; i++)
	{
		new_line();
	}
}
 
int main()
{
 	three_line();
 	return 0;
}
// 運作結果
// hello world
// hello world
// hello world
           

5.2 鍊式通路

把一個函數的傳回值作為另外一個函數的參數。
#include <stdio.h>
#include <string.h>

int main()
{
    printf("%d",printf("%d",printf("%d",43)));
    return 0;
}


// printf的傳回值是列印在螢幕上的字元的個數。
// 如果發生錯誤,将傳回負數。
// 運作結果
// 4321
           

6. 函數的聲明和調用

函數聲明:

  1. 告訴編譯器有一個函數叫什麼,參數是什麼,傳回類型是什麼。但是具體是不是存在,函數聲明決定不了。
  2. 函數的聲明一般出現在函數的使用之前。要滿足 先聲明後使用。
  3. 函數的聲明一般要放在頭檔案中的。

函數的定義:

指定函數的具體實作,交代函數的功能實作。
  • 将函數聲明放到

    Add.h

    檔案中。
    // #pragma one  -- 使頭檔案隻會被包含一次。 -- 等價于 ifndef
    
    #ifndef __ADD_H__    // 如果沒有定義過__ADD_H__ 
    #define __ADD_H__    // 定義__ADD_H__ 
    
    // 函數聲明
    int Add(int x, int y);
    
    #endif // !__ADD_H__
               
  • 将函數定義放到

    Add.c

    檔案中。
    // 函數的定義
    int Add(int x, int y)
    {
        int z = x + y;
        return z;
    }
               
  • test.c

    檔案中進行使用。
    #include <stdio.h>
    #include "add.h"  // 自己寫的頭檔案用 "" 雙引号引 
    
    // 導入靜态庫
    // #pragma comment(lib,"add.lib")
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int sum = 0;
    	sum = Add(a, b);
    	printf("%d\n", sum);
    	return 0;
    }
               

分塊去寫的好處:

  1. 多人協同;
  2. 封裝和隐藏;

7. 遞歸

7.1 什麼是遞歸?

程式調用自身的程式設計技巧稱為遞歸( recursion)。

遞歸做為一種算法在程式設計語言中廣泛應用。 一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞歸政策隻需少量的程式就可描述出解題過程所需要的多次重複計算,大大地減少了程式的代碼量。

遞歸的主要思考方式在于:把大事化小。

  • 最簡單的遞歸調用
    #include <stdio.h>
    int main()
    {
    	printf("haha\n");
    	main();
    	return 0;
    }
    // 該程式會進入死循環列印 haha ,棧記憶體耗光後會報如下圖所示的錯誤。
    // 函數調用向棧區申請空間
               
    第3章 函數1. 函數是什麼?2. C語言中函數的分類3. 函數的參數4. 函數的調用5. 函數的嵌套調用和鍊式通路6. 函數的聲明和調用7. 遞歸

7.2 遞歸的兩個必要條件

  • 存在限制條件,當滿足這個限制條件的時候,遞歸便不再繼續。
  • 每次遞歸調用之後越來越接近這個限制條件。

《函數棧幀的建立和銷毀》

練習

接收一個整型值(無符号),按照順序列印它的每一位。

例如:

輸入: 1234,輸出: 1 2 3 4.

#include <stdio.h>
void print(unsigned int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	scanf("%d", &num); // 1234

	// 遞歸
	// print(1234)
	// print(123)   4
	// print(12)  3 4
	// print(1) 2 3 4   
	//
	print(num);
	return 0;
}
           
第3章 函數1. 函數是什麼?2. C語言中函數的分類3. 函數的參數4. 函數的調用5. 函數的嵌套調用和鍊式通路6. 函數的聲明和調用7. 遞歸
第3章 函數1. 函數是什麼?2. C語言中函數的分類3. 函數的參數4. 函數的調用5. 函數的嵌套調用和鍊式通路6. 函數的聲明和調用7. 遞歸
編寫函數不允許建立臨時變量,求字元串的長度。
#include <stdio.h>
#include <string.h>

// 計數器的方式
//int my_strlen(char* str)
//{
//	int count = 0;
//	while(*str != '\0')
//	{
//		count++;
//		str++;
//	}
//	return count;
//}


// 遞歸的方法
// 把大事化小
// my_strlen("bit");
// 1+my_strlen("it");
// 1+1+my_strlen("t");
// 1+1+1+my_strlen("");
// 1+1+1+0
int my_strlen(char* str)
{
	if (*str != '\0')
		return 1 + my_strlen(str + 1);
	else
		return 0;
}

int main()
{
	char arr[] = "bit";
    // char* p = "bit"; // 也可以
	int count = 0;
	// int len = strlen(arr);
	// printf("%d \n", len);

	int len = my_strlen(arr);   // arr是數組,數組傳參,傳過去的不是整個數組,而是第一個元素的位址
	printf("len = %d \n", len);
	return 0;
}

#include <stdio.h>
#include <assert.h>
unsigned int my_strlen(const char* str)
{
	assert(str != NULL);
	int count = 0;
	while (*str++)
		count++;
	return count;
}
int main()
{
	char arr[] = "abcdef";
	printf("字元個數為:%u\n", my_strlen(arr));
	return 0;
}
           

7.3 遞歸與疊代

求n的階乘(不考慮溢出)
#include <stdio.h>
// 遞歸的方式
int Fac(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * Fac(n - 1);
}
int main()
{
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = Fac(n);  // 遞歸的方式
	printf("%d\n", ret);
	return 0;
}
           
求第 n 個斐波那契數 (不考慮溢出)。
#include <stdio.h>
// 遞歸方法
int Fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return  Fib(n - 1) + Fib(n - 2);
}
int main()
{
	// TDD -- 測試驅動開發
	int ret = Fib(30);
	printf("ret = %d\n", ret);
	return 0;
}
           

上述代碼存在問題:

  • 在使用

    Fac()

    函數求10000的階乘(不考慮結構的正确性),程式會崩潰。
  • 使用

    Fib()

    函數的時候,如果要計算第 50 個斐波那契數字的時候特别消耗時間。

為什麼呢?

  • 我們發現

    Fib()

    函數在調用的過程中很多計算其實在一直重複。

    如果我們把代碼修改一下:

    #include <stdio.h>
    
    int count = 0; // 全局變量
    
    int Fib(int n)
    {
    	if (n == 3)
    		count++;
    	if (n <= 2)
    		return 1;
    	else
    		return  Fib(n - 1) + Fib(n - 2);
    }
    int main()
    {
    	int ret = Fib(30);
    	printf("ret = %d\n", ret);
    	printf("3 == %d", count);
    	return 0;
    }
    
    // 運作結果
    // ret = 832040
    // 3 == 317811
    
    // 可以看到 在求第30個斐波那契數的時候3就被運算了 317811次。
               

那我們如何改進呢?

  • 在調試

    Fac()

    函數的時候,如果你的參數比較大,那就會報錯:

    stack overflow

    (棧溢出) 這樣的資訊。系統配置設定給程式的棧空間是有限的,但是如果出現了死循環,或者(死遞歸),這樣有可能導緻一直開辟棧空間,最終産生棧空間耗盡的情況,這樣的現象我們稱為棧溢出。

解決上述問題的方法:

  1. 将遞歸改成非遞歸。
  2. 使用

    static

    對象替代

    nonstatic

    局部對象。在遞歸函數設計中,可以使用

    static

    對象替代

    nonstatic

    局部對象(即棧對 象),這不僅可以減少每次遞歸調用和傳回時産生和釋放

    nonstatic

    對象的開銷, 而且

    static

    對象還可以儲存遞歸調用的中間狀态,并且可為各個調用層所通路。

采用非遞歸的方式實作上述案例:

// 循環的方式實作 求n的階乘 
int Fac(int n)
{
	int i = 0; 
	int ret = 1;
	for (i = 1; i <= n; i++)
		ret *= i;
	return ret;
}


// 循環方式求斐波那契數
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1; // 若将 c 指派 0,則 n =1 和 n = 2的情況無法計算,将 c 指派為1,則可以解決這個問題。
	while (n > 2 )
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
           

什麼時候用遞歸:

  1. 當解決一個問題遞歸和非遞歸都可以使用,且沒有明顯問題,那就可以使用遞歸。
  2. 當解決一個問題遞歸寫起來很簡單,非遞歸比較複雜,且遞歸沒用明顯問題,那就用遞歸。
  3. 如果說,用遞歸解決問題,寫起來簡單,但是有明顯問題,那就不能使用遞歸。

函數遞歸的幾個經典題目:

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

. 将遞歸改成非遞歸。

2. 使用

static

對象替代

nonstatic

局部對象。在遞歸函數設計中,可以使用

static

對象替代

nonstatic

局部對象(即棧對 象),這不僅可以減少每次遞歸調用和傳回時産生和釋放

nonstatic

對象的開銷, 而且

static

對象還可以儲存遞歸調用的中間狀态,并且可為各個調用層所通路。

采用非遞歸的方式實作上述案例:

// 循環的方式實作 求n的階乘 
int Fac(int n)
{
	int i = 0; 
	int ret = 1;
	for (i = 1; i <= n; i++)
		ret *= i;
	return ret;
}


// 循環方式求斐波那契數
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1; // 若将 c 指派 0,則 n =1 和 n = 2的情況無法計算,将 c 指派為1,則可以解決這個問題。
	while (n > 2 )
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
           

什麼時候用遞歸:

  1. 當解決一個問題遞歸和非遞歸都可以使用,且沒有明顯問題,那就可以使用遞歸。
  2. 當解決一個問題遞歸寫起來很簡單,非遞歸比較複雜,且遞歸沒用明顯問題,那就用遞歸。
  3. 如果說,用遞歸解決問題,寫起來簡單,但是有明顯問題,那就不能使用遞歸。

函數遞歸的幾個經典題目:

  1. 漢諾塔問題
  2. 青蛙跳台階問題
希望可以對大家有所幫助,如果有什麼不對的地方,還煩請指教,謝謝!

繼續閱讀