第九章 初窺天機之模塊化程序設計

哲理:

C語言共分爲兩類,一類是用戶自定義函數,一類是庫函數。用戶自定義函數是程序員在開發時根據需要,自己開發的函數。我們將會在本章進行詳細的講解。而庫函數就是別人已經寫好的函數庫,我們只需要拿過來用就行,比如printf函數,scanf函數,以及我們在上一章講解的和字符串處理相關的函數。

 

9.1 函數的概述

9.1.1 什麼是函數

說到函數,很多不瞭解編程語言的人在腦中會立刻浮現出數學方面或物理方面的函數,輸入變量x輸出變量y。事實上,C語言函數並不是這樣。“函數”是從function翻譯過來的,function的英文含義就是“函數”或“功能”。函數的本質就是完成一定功能的代碼。

每一個函數實現一個特定的功能,函數的名字反應其代表的功能。通過執行這些函數,實現預期的結果。如果函數是用來實現數學運算的,那麼該函數就是一個數學函數。

9.1.2 爲什麼使用函數

假如我們要製造一部智能手機,要事先生產各種部件,如CPU,電池,攝像機,屏幕,外殼等。而在組裝時,只用根據需要什麼器件直接取出安裝就行,而不是需要時臨時再去製造。這就是採用模塊化程序設計的思路。

在程序設計時,特別是在寫應用軟件時,所做項目往往都是一個大項目,那麼這就需要,把它分成若干個子程序模塊,每個模塊包含一個或多個功能函數,每個函數實現一個特定的功能。當每個子模塊實現的功能集合就是該項目的功能。如果把一個項目比喻成智能手機,那麼各個子模塊就是對應CPU,電池等。最後把這些部件組合成電腦。如圖9.1所示,一個C語言程序包含一個main函數和多個其他函數。在main函數裏面調用其他函數,其他函數之間是可以相互調用的,並且調用次數不限。

 

圖9.1 函數調用示意圖

 

程序設計中使用函數的目的就是把一個難以解決的大程序劃分成若干個獨立的子程序模塊,每個模塊解決一個或多個問題,把這些模塊組合起來,形成我們軟件。

 

9.2 函數的定義

9.2.1 函數原型

函數原型又稱函數聲明,是由函數返回類型、函數名和形參列表組成。形參列表必須包括形參類型,但是不必對形參命名。這三個元素被稱爲函數原型,函數原型描述了函數的接口。文字總是表示的不形象,下面讓我們換種形式,如下:

函數返回類型 函數名(參數類型 參數,參數類型 參數,...)

{

    函數體,實現我們想要的功能;

    [return 函數返回類型];

}

這個就是函數的整體框架,任何函數都是基於這樣一個框架來寫的。其中,‘[]’表示可選項,就是說如果返回值類型爲void,那麼就不需要返回值。函數返回值類型,參數類型等可以是任意類型,比如int,float,double,char等。下面我們首先就舉一個不帶返回值的實例來說明一下。如下所示:

void fun(int a, int b)

{

    printf(“%d\n”, a+b);

}

該函數的功能就是輸出a+b的結果。並不把結果返回去。下面我們看看帶返回結果的函數。如下所示:

int fun(int a, int b)

{

    return (a+b);

}

這個函數就是帶返回值的函數。

現在大家是不是對函數已經有了直觀的瞭解呢?那麼下面讓我們真正的去欣賞函數的魅力吧!

 

注意:

需要注意的是函數返回值類型和return返回的類型要求一致。

 

9.2.2 函數定義的方法及分類

上一節我們知道了函數定義也是有多種形式的,比如有無返回值。那麼函數到底都還有那些定義方式呢?我們接着看。

  1. 定義無返回值,有參數的函數。如9.2.1所示的是無返回值,有參數的函數定義。此處將不再舉例。
  2. 定義無返回值,無參數的函數。其一般形式如下:

void 函數名()

{

    函數體;

}

看僞代碼不如看真正的代碼來的給力。下面是一個具體可應用的實例。

void print()

{

    printf(“Hello C Program\n”);

}

  1. 定義有返回值,無參數的函數。其一般形式如下:

函數返回值 函數名()

{

    函數體;

    return 函數返回值;

}

對於返回值,我們前面已經說了,可以是任何類型,比如:char,int,double,float等。那麼具體的實例如下:

int random()

{

    return rand()%10;

}

這個實例中,我們用int作爲返回值。

4. 定義有返回值,有參數的函數。其一般形式如下:

函數返回值 函數名(參數類型1 參數1,參數類型2 參數2,...)

{

    函數體;

    return 函數返回值;

}

在這個形式中,多了參數,同樣我們也已經說了參數也可以是各種類型,不同的參數可以相同也可以不同。

double div(double num1,double num2,...)

{

    return num1/num2;

}

此處我們用double型參數表示。

5. 定義無返回值,有參數的函數。其一般形式如下:

void 函數名(參數類型1 參數1,參數類型2 參數2,...)

{

    函數體;

}

同樣我們還是舉一個例子,

void printChar(char c)

{

    printf(“%d\n”, c);

}

這個函數的功能就是輸出字符的ASCII值。

 

這五個是典型的函數具體形式,接下來我們看看如何調用函數吧!

 

9.3 函數的調用

9.3.1 函數聲明

首先,我們什麼都不說直接上一個程序來直觀的進行函數聲明的講解。

例9.1】簡單函數調用實例。

#include <stdio.h>
void printMessage();//函數聲明
int main()
{
	printMessage();//main函數調用printMessage函數
	return 0;
}
void printMessage()//函數
{
	printf("----------------------\n");
	printf("         *            \n");
	printf("        * *           \n");
	printf("       *   *          \n");
	printf("        * *           \n");
	printf("         *            \n");
	printf("----------------------\n");
}

這個程序就是一個函數調用的實例。在這裏我們就要講講什麼是函數聲明瞭。

在C語言中編譯系統是由上而下進行編譯的,如果被調用函數放在調用函數的後面,則需要對被調用函數在調用函數前面進行說明。否則編譯器無法識別函數,並且出現調用失敗。

比如我們上面例9.1所示,函數printMessage爲被調用函數,main函數爲調用函數。現在觀察發現被調用函數printMessage放在了調用函數main函數的後面,所以我們需要在第二行進行聲明(第二行是函數聲明),否則會出現編譯錯誤。大家可以嘗試把第二行去掉去編譯一下,多多嘗試,就會收穫多多。嘗試吧,少年!!!

那麼寫函數就一定需要函數聲明嗎?答案是:否定,滴!你只需要把被調用函數放到調用函數之前,C語言在自上而下編譯時,就會默認讀到還沒有調用的函數時,就會默認聲明該函數。

聰明的你一定會發現在這個程序中,第2行函數聲明,和函數第8~17行中的第8行除了最後多了一個分號外,是完全一模一樣啊!不錯!這就是函數的聲明,當你把一個函數模塊寫好,只需要把該模塊的第一行,複製一份放到頭文件下面,再加一個“;”,那麼你的函數聲明就完成了,是不是很簡單啊!其實程序就是這麼簡單。

注意:

1. 建議無論被調用函數放到哪裏,儘量都進行函聲明。

2. 函數可以聲明一次,但可以多次調用。

 

9.3.2 函數調用

其實,上一節實例9.1中我們就編寫函數的調用程序。在程序第五行調用printMessage函數。我們這一節會更加詳細的講解函數的調用。

首先我們此處寫一個main函數三個被調函數,第一個函數輸出一行“+”號,第二個函數輸出一個“hello world”字符串,第三個函數輸出數字。

第一個函數,輸出“+”號。

void printAdd()

{

    printf(“+++++++++++++++++++\n”);

}

第二個函數,輸出“hello world”。

int printStr()

{

    int a,b;

    a=2;

    b=3;

    printf(“   a*b=%d\n”, a*b);

    return a*b;

}

第三個函數,輸出數字。

void ifEven(int num)

{

    if(num%2 == 0)

    {

        printf(“%d是偶數!\n”, num);

    }

    else

    {

    printf(“%d是奇數!\n”, num);

    }

}

上面是三個被調函數的編寫,下面讓我們編寫主函數。

int main()

{

}

現在函數都已經寫好,我們也知道函數的執行是從main函數開始的,也就是說現在我們需要把上面兩個函數模塊寫到下面的main函數中去。

1. 首先我們寫成如下程序:

int main()

{

    printfAdd();

    return 0;

}

運行程序發現輸出的結果是一個字符串,如下:

++++++++++++++++++++++

Press any key to continue

2. 接着我們改成程序成如下形式

int main()

{

    int a;

    a = printStr();

    printf("   a*b=%d\n", c);

    return 0;

}

運行結果如下

   a*b=6

   a*b=6

Press any key to continue

  1. 最後我們把第三個函數寫出來,以便後面的對比。

int main()

{

    int d=5;

    ifEven(d);

    return 0;

}

運行結果如下

5是奇數!

Press any key to continue

 

從程序1中我們發現,對於無參數,無返回值的函數,我們只需要把函數名寫到main函數中即可實現調用,當然後面的“()”不要忘了。

從程序2中我們發現,對於返回值,無參數的函數,我們就需要定義一個和函數返回值類型相同的變量接收返回值。正如程序2中,函數的返回值類型是int型,那麼我們在main函數中定義一個int型變量a,接收函數的返回值。當然變量也可以不接收,或者定義成其他類型變量,不過此處我們暫時不考慮,後文會有涉及。大家現在只需按規定來就行。

從程序3中我們發現函數無返回值,有參數,我們只需要定義一個與參數類型相同的變量傳遞到函數中去即可。其中d爲5這個值就相當如賦值給函數ifEven中的num值,這個點我們將會在後面講到。

上面三個程序,就是我們講解的函數調用,相信大家通過對比三個程序的結果,就會對函數調用有更詳細的瞭解了,不過這些都是基礎,大家還需要努力喲!!!

關於函數的返回值的講解我們將會在下節給大家進行詳細的介紹,而帶參數的函數,我們也會在後面做詳細的講解。大家到時候就可以詳細的瞭解這些知識了。

此處,我們把上面的函數做一個簡單的整理,把代碼詳細的列出來,一遍大家能夠參考。

【例9.2】函數調用程序。

#include <stdio.h>
void printAdd();
int printStr();
void ifEven(int num);
void printAdd()
{
	printf("++++++++++++++++++++++\n");
}
int printStr()
{
	int a,b;
	a=2;
	b=3;
	printf("      a*b=%d\n", a*b);
	return a*b;
}
void ifEven(int num)
{
	if(num%2 == 0)
	{
		printf("      %d是偶數!\n", num);
	}
	else
	{
		printf("      %d是奇數!\n", num);
	}
}
int main()
{
	int c,d=5;
	printAdd();
	c = printStr();
	printf("      a*b=%d\n", c);
	ifEven(d);
	printAdd();
	return 0;
}

運行結果:

++++++++++++++++++++++

      a*b=6

      a*b=6

      5是奇數!

++++++++++++++++++++++

Press any key to continue

 

 

9.3.3函數返回值

上一節我們介紹了函數的整數返回值,大家已經對函數的返回值有了一個大概的理解了,這一節我們還將繼續講解函數的返回值。

首先,我們會不會有一個問題,那就是爲什麼要使用函數的返回值呢

此處寫有關函數返回值的定義

 

函數的返回值有多種類型,幾乎所有的類型都可以作爲返回值返回。比如void,int,double,float,枚舉類型,結構體類型,指針類型,甚至自定義類型等等。

那麼函數的返回值需要注意什麼呢?我們做一下四點說明。

(1) 函數的返回值是通過函數中的return語句獲得。如例9.2第32行所示,變量c就是從函數printStr()的return中獲取。那麼我們接着觀察這個程序的15和36行,我們發現15行返回的是一個表達式,36行返回的是一個整數0,那麼這就說明return既可以返回數值,也可以返回一個表達式。

 

  • 函數值的類型。在上一節我們舉過例子int類型的,說明返回值可以是int類型,當然也可以是float,double等任何數據類型,這就說明,如果要返回某個數據或某些數據,需要這些數據有確定的數據類型。比如以下四個函數所示:

int length(char str[]);

int max(int x, int y);

double min(double x, double y);

void print(char str[]);

 

  • 函數的返回值儘量要與定義的函數類型保持一致。這樣不會造成數據值的改變,丟失或錯誤。比如如下一段函數:

int length(char str[])

{

    int i=0;

    while(str[i] != ‘\0’)

    {

        i++;

    }

    return i;

}

這個函數的返回值爲int型,函數的定義類型也是int型,這就是函數我們所說的“函數的返回值儘量要與定義的函數類型保持一致”。假如我們return一個double類型的變量,而函數的定義類型是int型,那麼這就會造成數據精度的缺失,會把高精度的double型數據變量默認轉化爲低精度的int型數據。

 

事實上,在程序9.2中,ifEven函數已經實現了函數的數據傳遞,該函數傳遞的是整型變量。我們也9.4 函數調用中的數據傳遞

可以傳遞其他類型的變量。那麼數組可以傳遞嗎?聰明的你已經猜出來,不錯的確可以。形如int length(char str[]),其中的“char str[]”就是傳遞的字符型的一維數組。同理二維,三維也是完全可以的。我們將會在下面做詳細的介紹,不過在介紹之前,我們應該先介紹一下關於函數傳遞的基礎知識了。

 

9.4.1 形式參數和實際參數

什麼是形式參數?什麼又是實際參數?此處賣個關子,我們先來看一段程序。

【例9.3】通過函數傳參顯示數組長度。

#include <stdio.h>
int length(char str[])
{
	int i=0;
	while (str[i] != '\0')
	{
		i++;
	}
	return i;
}
int main()
{
	char s[]="I Love C Program";
	int len = 0;
	len = length(s);
	printf("數組長度:%d\n", len);
	return 0;
}

運行結果:

數組長度:16

Press any key to continue

 

觀察這個程序,首先第二行:int length(char str[]),這個函數中,str就是形式參數,又稱爲“形參”。char則爲形參的類型。其實,形式參數和實際參數是對應的,那麼調用函數中對應的數據就是實際參數了。程度第15行中的s就是實際參數。相當於把實際參數s賦值給形式參數str。實際參數又稱爲“實參”。

通過這個程序我們知道調用函數中的參數爲實際參數,而被調函數中的參數爲形式參數。我們把函數模塊換一種形式書寫會更加清楚了:

類型名 函數名(形式參數列表)

{

    函數體

}

 

說明:

  1. 實際參數可以是常量,變量,甚至是表達式。在之前的例子中我們已經講過,此處不再細說。
  2. 實參和形參的數據類型應該相同或者兼容。比如我們定義一個字符型數據類型,用一個整型變量接受你的字符型數據。如下:

char getChar()

{

    return ‘A’:

}

int a = getChar();

 

 

9.4.2 函數值傳遞

我們主要從使用角度介紹C語言的函數傳值。我們介紹的多爲應用而非理論,以求大家能夠通過實例掌握C語言。廢話不多說,我們接着說,理論!嗨!嗚嗚!

函數數據傳遞方式分爲兩類:值傳遞引用傳遞

值傳遞:數據只能從實參單向傳遞給形參,成爲“按值”傳遞。當基本類型變量作爲實參時,在函數調用過程中,形參和實參佔據不同的存儲空間,形參的改變對實參的值不產生影響。本節我們主要講函數的值傳遞。

引用傳遞:使實參和形參共用一個地址,即所謂“引用傳遞”。這種傳遞方式,無論對那個變量進行修改,都是對同一地址的內容進行修改,實參變量與它的引用變量,總是具有相同的值。函數的引用傳遞我們將會在下一節進行講解。

在前幾節關於函數值傳遞我們已經有所涉及,比如例9.2就是傳遞了一個整數,然後判斷是奇數還是偶數,並輸出結果。這個是普通的值傳遞。

例9.4】實現兩個數的加減乘除運算。要求用函數調用實現。

解題思路:首先,我們寫四個函數,分別是:加法函數,減法函數,乘法函數,除法函數。然後,用switch做一個可選項,選擇要進行那種計算。最後,把結果值返回主函數,並輸出結果。需注意,在做除法運算時,被除數不能爲0。

編寫程序:

#include <stdio.h>
void print();
double add(double a, double b);
double sub(double a, double b);
double mult(double a, double b);
double div(double a, double b);
void print()
{
	printf("****        四則運算        ****\n");
	printf("--------------------------------\n");
	printf("            1. 加法             \n");
	printf("            2. 減法             \n");
	printf("            3. 乘法             \n");
	printf("            4. 除法             \n");
	printf("--------------------------------\n");
	printf(">>>");
}
double add(double a, double b)
{
	return (a+b);
}
double sub(double a, double b)
{
	return (a-b);
}
double mult(double a, double b)
{
	return (a*b);
}
double div(double a, double b)
{
	return (a/b);
}
int main()
{
	int choose;
	double a, b, result;
	print();
	scanf("%d", &choose);
	switch(choose)
	{
	case 1:
		{
			printf("請輸入a, b值,以逗號分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			result = add(a, b);
			printf("%lf + %lf=%lf\n", a, b, result);
		}
		break;
	case 2:
		{
			printf("請輸入a, b值,以逗號分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			result = sub(a, b);
			printf("%lf - %lf=%lf\n", a, b, result);
		}
		break;
	case 3:
		{
			printf("請輸入a, b值,以逗號分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			result = mult(a, b);
			printf("%lf * %lf=%lf\n", a, b, result);
		}
		break;
	case 4:
		{
			printf("請輸入a, b值,以逗號分隔(比如1.0,3.0):");
			scanf("%lf,%lf", &a, &b);
			if (b<1e-6 && b>-1e-6)
			{
				printf("b值不能爲0。\n");
				return 0;
			}
			result = div(a, b);
			printf("%lf / %lf=%lf\n", a, b, result);
		}
		break;
	default:printf("輸入選項錯誤,請重新輸入。\n");
	}
	return 0;
}

運行結果:

****      四則運算     ****

----------------------------------------

          1. 加法

          2. 減法

          3. 乘法

          4. 除法

----------------------------------------

>>>2

請輸入a, b值,以逗號分隔(比如1.0,3.0):8.0,5.0

8.000000 - 5.000000=3.000000

Press any key to continue

 

程序分析:我們把加減乘除寫成函數調用的形式。方便大家對本節函數值傳遞的理解。

 

9.4.3 函數引用傳遞之指針變量

什麼是指針變量?簡單的說,就是在定義定義變量時,在變量名前面添加一個“*”。指針變量的定義方式如下:

變量名 *指針類型;

比如:int *p或者double *pt。這就是指針變量的定義。瞭解這些基本的知識我們就可以講解關於指針變量的引用傳遞了。

如果函數的形式爲指針類型時,對應的實參類型必須與形參的基類型相同。我們以下面這個實例進行說明。

例9.5】通過指針變量的引用傳遞帶回函數中的變量值。

解題思路:首先定義一個交換變量的函數,函數中變量定義爲指針變量,在調用函數中用傳地址的方式,傳遞變量值。並把函數中要交換的兩個變量帶回主函數,輸出結果。

編寫程序:

#include <stdio.h>
void swap(int *a, int *b);
void swap(int *a, int *b)
{
	int temp=0;
	temp = *a;
	*a = *b;
	*b = temp;
}
int main()
{
	int a=6, b=5;
	printf("a = %d, b = %d\n", a, b);
	swap(&a, &b);
	printf("a = %d, b = %d\n", a, b);
	return 0;
}

運行結果:

a = 6, b = 5

a = 5, b = 6

Press any key to continue

 

程序分析:觀察程序第3行和第14行,它們分別爲被調函數和調用函數。我們首先看第14行,如果調用函數中,如果要採用引用的方式,那麼被調函數需要用指針變量的形式定義形參。那麼就是第3行的定義方式。程序第13行和15行分別是執行被調函數前和被調函數後的結果。從結果中我們看出,如果採用指針傳遞的形式,會把被調用函數中的變量帶回主函數中。這就出現了程序的運行結果部分。

函數之間值的傳遞是單向傳遞,也就是說函數只能通過實參把值傳遞給形參,若形參值改變,對實參不會產生影響;把數據從被調函數返回到調用函數的唯一途徑就是通過return語句,且只能返回一個數據。若採用傳遞地址值的方式,即可以在被調函數中對調用函數中的變量進行引用,也可以把被調函數中改變的值傳回給調用函數。因此,通過改變形參的值,而讓實參的值也發生相應的改變,這樣就可以把多個數據從被調函數中返回調用函數(主函數)中。

 

9.4.4 函數引用傳遞之一維數組

我們從前面知道,在函數調用時,實際參數可以是常量、變量或表達式。其實,數組也是一種變量,那麼數組也是可以被調用的,用法與變量相同。另外,數組名既可以做實參,又可以做形參。在傳遞的是數組的第一個元素的地址。例9.3使用了一維數組調用。事實上,數組作爲函數參數傳遞時分爲兩種方式:一種是數組元素作爲函數實參傳遞,一種是函數名作爲參數傳遞。下面我們將對這兩種進行詳細說明。

 

一、 數組元素作爲函數實參傳遞

在主函數中把數組元素作爲實參傳遞傳遞給被調函數的形參時,稱爲“值傳遞”。另外,數組元素只能作爲實參,而不能當做形參。因爲數組在內存中是連續的一段存儲單元,不可能爲一個數組元素單獨分配存儲單元。

【例9.6】輸入一組學生成績,輸出最高成績。

解題思路:首先,輸入一組成績,保存到數組中。然後,通過兩兩比較找出最大的那個成績,並輸出結果。

編寫程序:

#include <stdio.h>
double maxGrade(double x, double y);
double maxGrade(double x, double y)
{
	return (x>y?x:y);
}
int main()
{
	double grade[10];
	double maxValue;
	int i;
	printf("請輸入10個學生的成績。\n");
	for ( i=0 ; i<10 ; i++ )
	{
		scanf("%lf", &grade[i]);
	}
	for (i=1, maxValue=grade[0]; i<10; i++)
	{
		maxValue = maxGrade(maxValue, grade[i]);		
	}
	printf("最高成績是:%0.1lf\n", maxValue);
	return 0;
}

 

運行結果:

請輸入10個學生的成績。

88.5 75 96 61 82 78.5 73 91 74.5 86

最高成績是:96.0

Press any key to continue

 

程序分析:通過這個程序希望大家加深理解函數的數組元素的傳遞。程序第19行,傳遞的就是grade數組中的數組元素。這個程序中有一個小技巧,那就是我們每次把最大的值直接賦值給maxValue,這樣每一次調用maxGrade函數後,獲取的就是最大值,直至整個成績數組比較完成。其中,初試默認grade[0]爲最大值。

 

 

二、 一維數組名作爲參數傳遞

之前我們講到函數之間進行數據傳遞時,數組元素可以作爲實參傳遞給形參,這時的數組元素與普通變量一樣,這種傳遞本質就是值傳遞。除此之外,還可以用數組名作函數參數(包括實參和形參)。我們已知用數組元素作爲實參時,對形參傳遞的是一個數值,而用數組名作爲函數實參時,對形參傳遞的是數組首元素的地址(數組的起始地址)。

【例9.7】通過傳遞數組名,實現數組賦值。

解題思路:在主函數中定義一個一維數組,然後定義一個函數,把主函數中的數組通過把數組名作爲實參的形式,傳遞給該函數,在該函數中實現數組初始化的功能。並在主函數中輸出最終結果。

編寫程序:

#include <stdio.h>
int main()
{
	void InitArr(int arr[], int count);
	int arr[5], i;
	InitArr(arr, 5);
	for (i=0; i<5; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}
void InitArr(int arr[], int count)
{
	int i;
	for (i=0; i<count; i++)
	{
		arr[i] = i+1;
	}
}

運行結果:

1 2 3 4 5

Press any key to continue

 

程序分析:在該程序中,我們發現第4行的函數聲明與以往把函數聲明寫到頭文件之下有所不同,這也是一種函數聲明的方式,這種聲明方式將不用考慮把函數模塊寫在main函數之前還是之後,或者說函數聲明之前還是函數聲明之後(注:另一種方式函數模塊必須放到函數聲明之後)。程序第6行以及第14行~21行中我們經過函數調用發現,如果要把一維數組作爲實參傳遞給形參,那麼在調用函數中我們只需要寫入一維數組名,在被調函數中我們需要在給相同類型數組名定義時,數組名後面需添加“[]”,這樣表示一維數組。如果是二維數組名傳遞,那麼就需要添加兩個“[][]”,並在最後一個“[]”中寫上對應的列數,比如a[5][5],那麼形參就寫成“a[][5]”。

 

【例9.8】通過函數傳遞實現計算學生的平均成績。

解題思路:與例9.7的解題思路類似,也是先定義一個函數用於成績的輸入,再定義一個函數實現計算成績平均值,最後在主函數中輸出成績的平均值。

編寫程序:

#include <stdio.h>
int main()
{
	void InitArr(double arr[], int count);
	double AverageScore(double arr[], int count);
	double grade[100], result;
	int num;
	printf("你要計算幾個學生的數學平均成績:");
	scanf("%d", &num);
	printf("請分別輸入這%d個學生的成績:", num);
	InitArr(grade, num);
	printf("平均成績計算中...\n");
	result = AverageScore(grade, 5);
	printf("這%d個學生的平均成績爲:%.1lf\n", num, result);
	return 0;
}
void InitArr(double arr[], int count)
{
	int i;
	for (i=0; i<count; i++)
	{
		scanf("%lf", &arr[i]);
	}	
}
double AverageScore(double arr[], int count)
{
	int i;
	double sum=0;
	for (i=0; i<count; i++)
	{
		sum = sum + arr[i];
	}
	return (sum/count);
}

運行結果:

你要計算幾個學生的數學平均成績:5

請分別輸入這5個學生的成績:66 78 83.5 90 89.5

平均成績計算中...

這5個學生的平均成績爲:81.4

Press any key to continue

 

程序分析:對於這個程序大家是不是有一種小系統的感覺呢?繼續努力我們將會寫出更大的系統。但是,不積跬步無以至千里。所以大家要學好類似的小程序才行啊。該程序的函數聲明還是和例9.7類似。第一個函數模塊實現了輸入學生的初試成績的功能。第二個函數模塊實現了計算學生平均成績的功能。程序第20~23行實現依次輸入學生的成績。程序第29~32行實現把所有學生的成績相加至sum中。程序第33行,返回成績的平均值,即成績和除以總人數。

 

在數組名作爲函數調用的過程中需要注意一下幾點:

  1. 實參和形參類型儘量保持一致。
  2. 實參數組和形參數組的大小保持一致。
  3. 如果是一維數組,形參可以不指定大小,但是一定要在形參名後添加“[]”。
  4. 一維數組傳遞的是第一個數組元素的地址。

 

9.4.5 函數引用傳遞之二維數組

無論一位數組元素還是一維數組名都可以作爲函數參數,那麼多維數組可以嗎?毫無疑問,當然可以。接下來我們主要以二維數組爲例,進行講解多維數組作爲函數參數的使用。  

要講解二維數值作爲實參傳遞給給形參時,我們需要先看看二維數組長什麼樣子,以方便我們理解二維數組的傳遞。

比如,我們定義一個4*5的二維數組作爲實參。

    int array[4][5];

那麼,把該二維數組傳遞給形參時,形參是如何定義的呢?形參可以定義爲如下兩種形式:

    int array[4][5] 或者 int array[][5]。

但是,不能定義爲:

    int array[][] 或者 int array[4][]。

那是因爲二維數組是由若干一位數組組成,在內存中數組是按行存放的。因此,在定義二維數組時,必須指定列數,即每一行中包含多少個元素,並且實參和形參數據類型相同,所以它們是由具有相同長度的一維數組所組成。

【例9.9】二維數組名作爲實參進行初始化

解題思路:經過上面的講解,我們該程序就會使用上面兩種合法的定義形式來進行講解。一種形式是int array[4][5],另一種形式是int array[][5]。該程序主要通過函數定義與輸出函數來對應上面的兩種形式的定義。

編寫程序:

#include <stdio.h>
int main()
{
	void InitArr(int arr[][5], int count);
	void Print(int arr[5][5], int count);
	int arr[5][5];
	InitArr(arr, 5);
	Print(arr, 5);
	return 0;
}
void InitArr(int arr[][5], int count)
{
	int i, j;
	for (i=0; i<count; i++)
	{
		for (j=0; j<count; j++)
		{
			arr[i][j] = i*count+j+1;
		}
	}
}
void Print(int arr[5][5], int count)
{
	for (int i=0; i<count; i++)
	{
		for (int j=0; j<count; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

運行結果:

1 2 3 4 5

6 7 8 9 10

11 12 13 14 15

16 17 18 19 20

21 22 23 24 25

Press any key to continue

 

程序分析:程序第11~21行和程序第22~32行就是兩個數組調用表現形式,它們的聲明分別爲第3行和第4行。首先,程序第11~21行表示第一種形參的接受形式,即int arr[][5]。由於程序是按照行存放的,所以當確定列數時,行數自然就確定了。然後,程序第22~32行表示第二種形參的接受形式,即int arr[5][5]。這個形式直接就確定了接受的行數和列數,所以不需要去特殊的講解。

 

9.5函數的嵌套調用

C語言中不允許做嵌套的函數定義。因此各函數之間是平行的,不存在上一級函數和下一級函數的問題。但是C語言允許在一個函數的定義中出現對另一個函數的調用。這樣也就出現了函數的嵌套調用,即在被調用函數中有調用其他函數。這與其他語言的子程序嵌套的情形是類似的。

圖9.2 多層函數模塊嵌套調用

 

如圖9.2所示多層嵌套,其執行過程如下:

  1. 執行main函數至調用函數1。
  2. 執行調用函數1至調用函數2。
  3. 這樣依次執行,,,
  4. 一直執行到函數N。
  5. 由函數N返回上一層調用函數N-1。
  6. 依次返回至函數2。
  7. 由函數2返回函數1。
  8. 繼續執行main函數的剩餘部分直到結束。

 

【例9.10】計算之和。

解題思路:首先,主函數我們只需要輸入要求這四個數據。然後,在第一個調用函數中我們計算“+”兩邊的和。接着,計算每個數次方的值。最後,依次返回上一級函數,直至返回帶主函數中輸出結果。

編寫程序:

#include <stdio.h>
int calc_sum(const int, const int, const int, const int);
int calc_fact(const int base,const int num);
int calc_sum(const int a,const int num1, const int b, const int num2)
{
	return (calc_fact(a, num1)+calc_fact(b,num2));
}
int calc_fact(const int base,const int num)
{
	if (num<=0)
	{
		return 1;
	}
	return base*calc_fact(base, num-1);
}
int main()
{
	int a,b,num1,num2, result=0;
	printf("請輸入兩個數值及對應的階乘:");
	scanf("%d%d%d%d", &a, &b, &num1, &num2);
	result = calc_sum(a,num1, b, num2);
	printf("result:%d\n", result);
	return 0;
}

運行結果:

請輸入兩個數值及對應的階乘:2 3 3 2

result:17

Press any key to continue

 

程序分析:程序第2~3行是兩個函數的聲明。在main函數中接受要輸入的數據,如程序20行。然後調用函數calc_sum()函數計算立方與平方的和,就是8和9。接着進入calc_sum()函數,調用calc_fact()函數計算每個數據的值,就是計算2的立方和3的平方。通過遞歸調用該函數,計算出想要的結果,具體遞歸調用我們將會在下一節進行講解。最後,把獲取的值依次返回到主函數中,輸出結果。

圖9.3 calc_fact()函數流程圖

以2的立方爲例,我們看一下具體的calc_fact()函數的流程圖。當把底數2和指數3傳遞到該函數後,判斷3是不是小於等於0,顯然3大於0,程序執行到2*calc_fact(2,3-1),此時calc_fact(2,3) = 2*calc_fact(2,3-1),同時函數第二個參數減1,變成calc_fact(2,2)。然後,函數calc_fact()接受變量2,判斷2是不是小於等於0,顯然不是,接着是執行到2*calc_fact(2,2-1),此時calc_fact(2,2)=2*calc_fact(2,2-1),同時第二個參數減1,變成calc_fact(2,1)。接着,函數calc_fact()接受變量1,判斷1是不是小於等於0,顯然不是,接着執行到2*calc_fact(2,1-1),此時calc_fact(2,1)=2*calc_fact(2,1-1),同時第二個參數減1,變成calc_fact(2,0),顯然0小於等於0。那麼calc_fact(2,0)=1。依次放回到原來的數據:

calc_fact(2,0)=1;

calc_fact(2,1)=2*calc_fact(2,0)=2*1=2;

calc_fact(2,2)=2*calc_fact(2,1)=2*2=4;

calc_fact(2,3)=2*calc_fact(2,2)=2*4=8;

 

最後把calc_fact(2,3)返回到函數calc_sum()中,就是程序的第6行:

    return (calc_fact(a,num1)+calc_fact(b,num2));

也就是:

    return (calc_fact(2,3)+calc_fact(3,2));

也就是:

    return (8+calc_fact(3,2));

用相同的方法計算calc_fact(3,2),獲得的值爲9。

那麼:

    return (8+9);

就會主函數中獲得calc_sum()的返回值,就是程序第21行,result值就是17。最後輸出結果。

 

9.6 函數遞歸調用

一個函數在它的函數體內調用它自身稱爲遞歸調用,這種函數稱爲遞歸調用。執行遞歸調用將反覆調用其自身,每調用一次就進入新的一層。

在上一節函數嵌套調用中的實例程序9.10,函數calc_fact()就是遞歸函數。如果把圖9.2中的函數1,函數2,…,函數N,看做是同一個函數的話,那就是函數的遞歸調用。

函數調用的大概過程:

  1. 將調用函數的上下文入棧;
  2. 調用被調用的函數;
  3. 被調函數執行;
  4. 調用函數上下文出棧,繼續執行後繼指令。

所以在函數調用過程中調用函數是不會退出的,被調函數的內存只有在返回調用函數後纔會釋放。

 

【例9.11】用遞歸求出n!。

解題思路:首先n!=1•2•3…n,那麼使用遞推的方法:

1!=1;

2!=2•1;

3!=3•2•1;

n!=n•(n-1)•(n-2)…2•1;

因此,基於數學的方法我們總結出遞歸公式:

編寫程序:

#include <stdio.h>
int main()
{
	int fact(int);
	int num = 0, n;
	printf("請輸入一個整數:");
	scanf("%d", &n);
	num = fact(n);
	printf("%d!=%d\n", n, num);
	return 0;
}
int fact(int n)
{
	if(n<=1)
	{
		return 1;
	}
	return n*fact(n-1);
}

運行結果:

請輸入一個整數:5

5!=120

Press any key to continue

 

程序分析:程序第4行爲遞歸函數的聲明,程序第8行調用遞歸函數。本程序的核心就是第12~19行的遞歸函數,如圖9.4所示的遞歸流程圖,執行順序爲1,2,3,...,10。main函數中第一次傳遞到遞歸函數,此時n是5,然後開始執行遞歸。n=5不滿足n<=1,執行n*fact(n-1),即5*fact(4)。由於要返回5*fact(4)的值,可是fact是函數,不是確定值,所以要繼續執行該函數。此時n=4不滿足n<=1,執行n*fact(n-1),即4*fact(3)。同樣fact是函數,繼續執行fact函數。此時n=3不滿足n<=1,執行n*fact(n-1),即3*fact(2)。同理,直到執行到n=1時,滿足n<=1,執行fact(1),返回整數1,以此返回上一級,2*1,3*2*1,4*3*2*1,5*4*3*2*1,最後獲取結果120。

如下爲遞歸程序的執行流程:

圖9.4 fact函數遞歸流程圖

遞歸的基本原理:

  1. 每次函數調用都會有返回值,當程序執行到某一級遞歸的結尾處時,它會轉移到前一級遞歸的下一條命令繼續執行。
  2. 遞歸函數中,位於遞歸調用前的語句和各級被調函數具有相同的順序。
  3. 每一級函數調用都有自己的私有變量。
  4. 遞歸函數中,位於遞歸調用語句後的語句的執行順序和各個被調用函數的順序相反。
  5. 雖然每一級遞歸有自己的變量,但是函數代碼並不會得到複製。
  6. 遞歸函數中必須包含可以終止遞歸調用的語句。

 

9.7 變量的作用域和生存週期

9.7.1 變量屬性

變量也有屬性,變量的屬性共分爲六種,它們分別是:名稱、地址/左值、值/右值、存儲類型、作用域、生存週期。下面我們將詳細介紹這幾種方式。

 

  • 名稱

名稱定義類似於標識符的定義,當多個名字訪問同一存儲地址時,就稱這些名字爲別名。但是如果使用別名過多不利於程序的可讀性,然而卻存在於任何一門語言中。

例如:

int a = 1;

int b = a;

 

其中,b作爲a的別名,都指向同一個地址,值爲1。當a的值發生改變後,b卻不會改變。我們會發現很多時候多個函數有相同的參數名。

 

  • 地址/左值

計算機中所有的數據都是存放在存儲器中的,一般把存儲器中的一個字節稱爲一個存儲單元。爲了能夠正確的訪問這些存儲單元,需要爲每個存儲單元編號,根據編號就可以準確的找到該內存單元。內存單元的編號稱爲地址。

比如上面的a就是地址,地址a指向的存儲空間存放的就是1。

 

  • 值/右值

值即變量的值,表示與該變量相關聯的存儲單元的內容。變量的值有時候也稱爲變量的右值,因爲變量的值常被用於賦值語句的右邊。比如:a=1,表示左邊變量a接收右邊變量的值1的賦值。

 

  • 存儲類型

變量的存儲類型指系統針對變量存儲方式的規定。根據系統的存儲方式可以分爲兩類。一類是靜態存儲方式。一類是動態存儲方式。

  1. 靜態存儲方式:指在程序運行期間,系統對變量固定地分配存儲空間。即一旦分配,不在變化直到整個程序運行結束。比如:int a[5],表示靜態的爲變量a申請5個int型的存儲空間長度。
  2. 動態存儲方式:指在程序運行期間,系統對變量動態地分配存儲空間。即程序運行期間,可以根據程序需求,動態分配。比如:通過malloc等函數動態申請內存空間。

 

存儲類型既說明了變量的存儲單元,有說明了變量的生存的時間和作用域。對於存儲類型有四種限定符。在此之前,我們需要了解一個事實:在某一個程序文件中定義的全局變量和函數均默認爲外部的,即跨文件的。

  1. 自動變量auto:指不加說明的局部變量。變量生存週期結束由系統自動釋放其存儲空間,所以稱爲自動。
  2. 寄存器變量register:爲提高程序執行效率,允許將局部變量的值存放於寄存器中(注意不是內存中),因爲寄存器有限,所以不提倡把所有的變量都存放與寄存器中。事實上,如果存放過多數據到寄存器中,執行效率不會提高,程序還是會自動把大部分變量放到內存中。
  3. static變量:可作用於局部變量和全局變量,故可分爲:局部靜態和全局靜態。靜態說的是:生存週期。而局部或全局說的是:作用域。
  4. 以extern聲明的變量:指全局變量,若要在其他文件中使用,需要加以聲明,方法:使用前用extern作外部聲明即可。通常放於文件開頭,並且對於函數而言,通常省略關鍵字extern。

 

  • 作用域

變量的作用域是指變量的有效範圍,它從空間角度體現變量的特性。變量的作用域細分可分爲六種:全局變量作用域、局部變量作用域、語句作用域、類作用域、命名空間作用域、文件作用域。

常用的就是全局變量作用域和局部變量作用域。

 

  • 生存週期

變量的生存週期指從變量創建到刪除所經歷的時間段,它是從時間角度衡量變量的特性。其生存週期共分爲三類。

  1. 動態生存期:指存放在“堆區”中的數據。創建、刪除均有程序員自己完成。
  2. 局部生存期:指存放在“棧區”中的數據。
  3. 靜態生存期:指存放在“數據區”中的數據。程序一運行,它們就存在;程序一結束,它們由系統自動釋放。

 

9.7.2 局部變量

本文通過程序來說明什麼是局部變量。

【例9.12】局部變量之計算正方形,長方形,梯形的面積。

解題思路:通過定義三個函數以及主函數來說明什麼是局部變量。

編寫程序:

#include <stdio.h>
double square(double edge)
{
	double area = edge*edge;
	return area;
}
double rectangle(double length, double width)
{
	double area = length*width;
	return area;
}
double trapezoid(double upper, double bottom, double height)
{
	double area = (upper+bottom)*height/2;
	return area;
}
int main()
{
	double a=2.4, b=3.6, c=4.8;
	double squareArea,rectangleArea,trapezoidArea;
	squareArea = square(a);
	printf("Square area:%lf\n", squareArea);
	rectangleArea = rectangle(a, b);
	printf("Rectangle area:%lf\n", rectangleArea);
	trapezoidArea = trapezoid(a, b, c);
	printf("Trapezoid area:%lf\n", trapezoidArea);
	return 0;
}

運行結果:

Square area:5.760000

Rectangle area:8.640000

Trapezoid area:14.400000

Press any key to continue

 

程序分析:首先在main函數中定義double型變量的有:a,b,c,squareArea,rectangleArea,trapezoidArea六個局部變量。它們的作用範圍就在main函數中,其他函數中無法使用這六個變量。當main函數結束時,這六個變量自動釋放。同樣在square函數中,局部變量爲形參edge和area,它們隨着square函數的調用而產生,結束而釋放,作用範圍就在square函數中,其他函數中無法使用這兩個變量。針對rectangle函數和trapezoid函數也是同理。

通過上面程序的說明我們發現,在一個函數內部定義的變量只在該函數範圍內有效,即只能在該函數中被使用,其他函數不能使用這些變量,如上例中所示,這些變量稱爲局部變量。還有一種局部變量定義的方式就是複合語句內定義。在複合語句內定義的變量只能在複合語句內使用,在複合語句外不能使用這些變量,這些變量也稱爲局部變量。

比如:

void func()

{

    int a;

    ┇

    {

        int b;

        ┇

    }

}

其中,變量b就是複合語句內的變量,即複合語句內的局部變量。變量b只在複合語句中有效。

 

此處有幾點說明:

  1. 不同函數中可以使用同名的變量,它們互不影響。例如上面程序中的變量area。
  2. 形參也是局部變量。例如上面程序中的edge,length,width等變量也是局部變量。
  3. 函數內部的複合語句內也可以定義局部變量,這些變量的使用範圍就在複合語句內。

 

9.7.3 全局變量

在上一節中,我們已經介紹了局部變量,局部變量就是在函數內部定義的變量稱爲局部變量,而在函數外部定義的變量則稱爲全局變量,或者外部變量。全局變量作用範圍是從定義變量開始到該源文件結束,即全局變量可以被本文件中其他函數共同使用。

【例9.13】全局變量之計算正方形,長方形,梯形的面積。

解題思路:通過定義三個函數以及主函數來說明什麼全局變量。

編寫程序:

#include <stdio.h>
double a=2.4, b=3.6, c=4.8;
double squareArea,rectangleArea,trapezoidArea;
double area;
void square(double edge)
{
	area = edge*edge;
}
void rectangle(double length, double width)
{
	area = length*width;
}
void trapezoid(double upper, double bottom, double height)
{
	area = (upper+bottom)*height/2;
}
int main()
{
	square(a);
	printf("Square area:%lf\n", area);
	rectangle(a, b);
	printf("Rectangle area:%lf\n", area);
	trapezoid(a, b, c);
	printf("Trapezoid area:%lf\n", area);
	return 0;
}

運行結果:

Square area:5.760000

Rectangle area:8.640000

Trapezoid area:14.400000

Press any key to continue

 

程序分析:該實例程序中定義幾個double型的全局變量:a,b,c,squareArea,rectangleArea,trapezoidArea,area。在改程序中它們的作用範圍是從定義變量開始直到整個程序結束,即程序結束時,這七個全局變量自動釋放。那麼在square函數中的形參edge和area仍然是局部變量,它們隨着square函數的調用而產生,結束而釋放,作用範圍就在square函數中,其他函數中無法使用這兩個變量。針對rectangle函數和trapezoid函數也是同理。但是這三個函數中的area卻是全局變量,我們不僅可以在這三個函數中使用,也可以在main函數中使用。比如square函數中把計算的結果賦值給area,然後在main函數中輸出area的值,這就是全局變量的使用。當然也可以在main函數中計算area的值,然後在被調函數中輸出結果,這就形成了常用的“顯示函數”。

針對什麼是局部變量?什麼是全局變量?可以用一句話概括:在函數內部定義的變量叫做局部變量,在函數外定義的變量叫做全局變量。在一個函數中可以同時存在局部變量和全局變量。

建議在不明白的情況下,不要過多的使用全局變量,它會使程序的可讀性、清晰性、通用性等會大大降低。因爲如果在一個源程序中針對某一處的全局變量的值進行改變,那麼整個程序中該全局變量的值對於另一處的結果造成不利的影響。準確說例9.13是一個很不嚴謹的程序,就是因爲全局變量的不當使用。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章