本文大綱:
6.1 函數概述
C語言中,模塊用函數來實現
函數分類以下兩種:
1)標準庫函數:用戶不需要定義可直接使用。如scanf()、printf()等
2)用戶自己定義的函數
例子:
從鍵盤輸入兩個正整數m和n,求m!/(m-n)!的值
函數的作用:
1)可方便地使用其他人已經編寫的代碼,就像調用系統提供的庫函數
2)可以在後續程序中使用自己編寫的代碼
3)實現結構化程序設計的基本思想
結論:
1)一個完整的C程序可以由若干個函數組成,其中必須有一個且只能有一個主函數。從主函數開始執行,而其他函數只能被調用
2)完整的C程序中的所有函數可以放在一個文件中,也可以放在多個文件中
6.2 函數的定義與聲明
先定義-->後聲明-->再使用
首先定義函數的數據類型、存儲類型、函數體
然後才使用
3個概念:函數定義、函數調用、函數聲明
函數定義:定義函數的功能(未經定義的函數不能使用)。可分爲庫函數和用戶自定義函數兩種。
函數調用:執行一個函數;調用函數時,如果有參數首先傳參數,程序由主調函數跳轉到被調函數體內的第一條語句開始執行,執行完被調用函數體內的最後一條語句或中途遇到return語句時,又返回到主調函數繼續向下執行
函數聲明:通知編譯系統該函數已經定義過了;庫函數不需要寫出函數聲明,只需要在程序前面用#include包含具有該庫函數原型的頭文件即可;用戶自定義函數,如果函數定義的位置在函數調用之後,則前面必須有函數聲明;如果函數定義放在函數調用之前,則可省略函數聲明
6.2.1
函數定義:
函數定義的一般形式:
[函數類型] 函數名字([形式參數表])
{
[聲明部分]
[執行語句]
}
說明:一個函數(定義)由函數頭(函數首部)和函數體兩部分組成。
(1)函數頭(首部):說明函數類型、函數名稱、參數
1)函數類型:函數返回值的數據類型;基本類型或構造類型;默認int;不返回值則定義爲void類型
2)函數名:給函數取的名字,以後用這個名字調用;命名規則與標識符相同
3)函數名後面是形式參數表,也可以是沒有參數,但()不能省略,這是格式的規定。形式參數表是說明形式參數的類型和形式參數的名稱,各個形式參數之前用,分隔。
(2)函數體:函數頭下方用一對{}括起來的部分。如果函數體內有多個{},最外層是函數體的範圍。函數體一般包括聲明部分和執行部分。
1)聲明部分:定義本函數所使用的變量和進行的有關聲明(如函數聲明)
2)執行部分:程序段,即由若干條語句組成的命令序列(可以在其中調用其他函數)
函數不能單獨運行,函數可以被主函數或其他函數調用,也可以調用其他函數,但不能調用主函數
6.2.2 函數的參數和返回值
函數的參數分爲形式參數和實際參數
形式參數(形參,形參的本質就是變量):函數定義時設定的參數。
例:int max3(int x,int y,int z)中的x,y,z就是形參,都是整型
實際參數(實參):調用函數時所使用的實際的參數。
例:主函數中調用max3函數的語句namx=max3(n1,n2,n3)中,n1,n2,n3就是實際參數,都是整型
形參和實參的功能:數據傳遞
只有發送函數調用時,主調函數把實參的值傳遞給被調函數的形參(也就是實參給形參賦值),從而實現主調函數向被調函數的數據傳遞。保證實參與形參的類型一致、個數一致、順序一致
C語言可以從函數(被調函數)返回值給調用函數。通過return語句返回值,使用return語句能夠返回一個值或不返回值(此時函數類型是 void)。
return語句格式
return [表達式];
return(表達式)
注意:
1)main是函數,必須有返回值,默認返回int類型。exit(0)或return(0)
2)函數的類型就是返回值的類型,return語句中表達式的類型與函數類型一致,如果不一致,一函數類型爲準
3)如果函數沒有返回值,函數類型應該說明void(空類型)
函數聲明:
函數定義的位置隨意。
1)函數定義位置在前,函數調用在後,不必聲明,編譯程序產生正確的調用格式
2)函數定義在調用它的函數之後或函數在其他源程序模塊中,且函數類型不是整型,在函數使用前對函數進行聲明
函數聲明格式:
函數類型 函數名([形式參數表]);
6.3 函數的調用
一個函數調用另外一個函數稱爲函數調用,其調用者稱爲主調函數,被調用者稱爲被調函數。
三種方式:
1)函數語句形式
只進行某些操作而不返回函數值,這時的函數調用可作爲一條獨立的語句
2)函數表達式形式
函數作爲表達式的一項,出現在表達式中,以函數返回值參與表達式的運算,這種方式要求函數必須有返回值
3)函數實參形式
函數作爲另一個函數調用的實際參數出現。這種情況是把函數的返回值作爲實參進行傳送,因此要求該函數必須有返回值
函數語句形式:
函數表達式形式:
函數實參形式:
6.3.2 函數參數的傳遞形式
實參與形參的傳遞方式有兩種:
值傳遞和地址傳遞
值傳遞:參數傳遞的是數據本身;數值只能由實參傳遞給形參,形參不能反過來傳遞給實參,即傳值是單向的。形參的任何變化不會影響到實參。
過程:
1)發生函數調用時,系統臨時創建形參變量
2)實參將其數值複製一份給形參變量
3)函數調用過程中,形參的任何改變只發生在被調函數內部,不會影響到實參
4)當被調函數運行結束返回主調函數時,形參的存儲空間被自動釋放
例:swap()函數
#include "stdio.h"
void swap(int a,int b) //形參a、b
{
int t;
printf("(2)子函數開始時:a=%d,b=%d\n",a,b); //輸出子函數中交換操作前的數值
t=a;
a=b;
b=t;
printf("(3)子函數結束時:a=%d,b=%d\n",a,b); //輸出子函數中交換操作後的數值
}
main()
{
int x=2,y=4;
printf("(1)子函數調用前:x=%d,y=%d\n",x,y); //輸出swap()函數調用前的數值
swap(x,y);
printf("(4)子函數調用後:x=%d,y=%d\n",x,y); //輸出swap()函數調用後的數值
return 0;
}
--實參向形參傳值是單向傳遞
例:定義函數,求極值
#include "stdio.h"
main()
{
int n;
void s(int n);
printf("請輸入n的值:\n");
scanf("%d",&n);
s(n);
printf("n=%d\n",n); //實參的值不隨形參的變化而變化
}
void s(int n)
{
int i;
for(i=n-1;i>=1;i--)
{
n=n+i;
}
printf("n=%d\n",n);
}
6.3.3 函數的嵌套調用
C語言中函數定義都是互相平行、獨立的,也就是說在定義函數時,一個函數內不能包含另外一個函數。
C語言不能嵌套定義函數,但可以嵌套調用函數,也就是說,在調用一個函數的過程中,又調用另外一個函數。
例:計算機平方階層
#include "stdio.h"
long f1(int p)
{
int k;
long r;
long f2(int q); //聲明
k=p*p;
r=f2(k);
return r;
}
long f2(int q)
{
long c=1;
int i;
for(i=1;i<=q;i++)
{
c=c*i;
}
return c;
}
main()
{
int i; //i是數字
long s=0;
for(i=2;i<=3;i++)
{
s=s+f1(i);
}
printf("s=%ld\n",s);
}
6.3.4 函數的遞歸調用
一個函數在它的函數體內調用它自身的過程稱爲遞歸調用。
直接遞歸調用或間接遞歸調用
例:有5個人做在一起,問第5個人多少歲?
#include "stdio.h"
main()
{
int age(int n);
printf("%d\n",age(5));
return 0;
}
int age(int n)
{
int c; //用c作爲存放函數的返回值的變量
if(n==1)
{
c=10;
}
else
{
c=age(n-1)+2;
}
printf("\t%d\n",c);
return(c);
}
例:求n!
#include "stdio.h"
main()
{
int n;
long ff(int n);
long y;
printf("請輸入n的值:\n");
scanf("%d",&n);
if(n<0)
{
printf("n<0!input error!");
}
else
{
y=ff(n);
printf("%d!=%ld",n,y);
}
return 0;
}
long ff(int n)
{
long f;
if(n==0||n==1)
{
f=1;
}
else
{
f=n*ff(n-1);
}
return (f);
}
一個遞歸問題可分爲:回溯 和 遞推 兩個階段;且必須要有一個遞歸過程的邊界條件。
兩個階段:
1)遞推階段:將原問題不斷地分解成爲新的子問題,逐漸從未知的向已知的方向推測,最終達到自己已知的條件,即遞歸結束條件,這時遞推階段結束。
2)迴歸階段:從已知條件出發,按照“遞推”的逆過程,逐一求值迴歸,最終到達“遞推”的開始處,結束迴歸階段,完成遞歸調用
要有遞歸的終止條件! 遞歸=遞歸方式+遞歸條件
6.4 局部變量和全局變量
函數中:形參變量只在被調用期間才分配內存單元,調用結束後立即釋放。表明形參變量只有在函數內纔有效,離開該函數就不能再使用了。
這種變量有效性地範圍稱爲變量地作用域。變量說明方式不同,作用域也不同。
按作用域範圍可分爲:局部變量 和 全局變量。
6.4.1 局部變量
局部變量也稱爲內部變量。局部變量是在函數內作定義和說明。作用域僅限於函數內,離開該函數後再使用這種變量就是非法的。
注意:
- 主函數中定義的變量只能在主函數中使用,不能在其他函數中使用。同時,主函數也不能使用其他函數中定義的變量。主函數也是一個函數,與其他函數是平行關係。
- 形參變量是屬於被調函數的局部變量,實參變量是屬於主調函數的局部變量
- 允許在不同的函數中使用相同的變量名,它們代表不同的對象,分配不同的單元,互不干擾,也不會發生混淆
- 在複合語句中也可以定義變量,其作用域只在複合語句範圍內。
6.4.2 全局變量
全局變量也稱爲外部變量,在函數外部定義的變量。
不屬於某一個函數,而屬於一個源程序文件,其作用域是從定義變量的位置開始到本源文件結束
如果全局變量在文件的開頭定義,則在整個文件範圍內均可以使用該全局變量;如果不在文件的開頭定義,又想在定義點之前使用該全局變量,需要用exterm進行聲明。
變量的聲明:說明變量的性質,並不分配存儲空間
變量的定義:即爲變量分配存儲空間
例:輸入正方體的長、寬、高,分別爲l,w,h,求體積及三個面的面積。
#include "stdio.h"
int s1,s2,s3; //作用域爲整個程序
main()
{
int v,l,w,h;
int vs(int a,int b,int c);
printf("請輸入長,寬,高的值:\n");
scanf("%d %d %d",&l,&w,&h);
v=vs(l,w,h);
printf("v=%d,s1=%d,s2=%d,s3=%d\n",v,s1,s2,s3);
return 0;
}
int vs(int a,int b,int c)
{
int v;
v=a*b*c;
s1=a*b;
s2=b*c;
s3=a*c;
return v;
}
說明:
- 對於局部變量的定義和說明,可以不加區分。全局變量定義必須在所有的函數之外,且只能定義一次。
全局變量的定義:
類型說明符 變量名,變量名……
全局變量的聲明:
extern 說明符 變量名,變量名……
全局變量在定義時就分配了內存單元,全局變量在定義時也可作初始賦值,但不能在聲明時賦初始值,全部變量的聲明只是表明在函數內要使用該全局變量。
例:編寫程序,輸入兩個數,調用函數找出最大值
#include "stdio.h"
int a,b; //全局變量的定義
main()
{
extern int a,b; //全局變量的聲明
int mymax(int x,int y);
printf("請輸入a,b的值:\n");
scanf("%d %d",&a,&b);
printf("max=%d\n",mymax(a,b));
return 0;
}
int mymax(int x,int y)
{
int z;
if(x>y)
{
z=x;
}
else
{
z=y;
}
return z;
}
- 全局變量只在所有函數之外定義一次;全局變量可以聲明多次,哪個函數內要用到在其後面定義的變量,就需要在該函數內對該全局變量進行聲明。
在同一個源文件中,允許全局變量與局部變量同名。在局部變量的作用範圍內,全局變量被屏蔽,即它不起作用。
例:全局變量與局部變量同名。
在局部變量的作用範圍內,全局變量被“屏蔽”,即全局變量不起作用
- 如果沒有全局變量,函數只能通過參數與外界發生數據關係,有了全局變量以後,增加了一條與外界傳遞數據的渠道。
聯繫太多,降低模塊的獨立性
6.5 變量的存儲屬性
1. 用戶程序的存儲分配
程序區 |
靜態存儲區 |
動態存儲區 |
程序區:存放程序;
靜態存儲區:在程序開始執行時就分配的固定存儲單元,如全局變量
動態存儲區:在函數調用過程中進行動態分配的存儲單元,如函數形參、自動變量
2. 變量的存儲類型
變量和函數有兩個屬性:操作屬性和存儲屬性
操作屬性:數據類型
存儲屬性:變量的存儲類型、變量的生存期和變量的作用域
變量劃分:空間角度,全局變量和局部變量;生存期角度,永久存儲和動態存儲;
永久存儲:從程序的開始到結束;動態存儲:在程序的過程中
按存儲屬性四個分類:register(寄存器)、auto(主存)、static(主存)、extern(主存)
在定義一個變量時,除了指定其數據類型外,還可以執行其存儲屬性。
6.5.1 自動變量(auto)
自動變量爲局部變量。說明符:auto
當程序的一個局部要使用某些自動變量時,說明形式:
[auto] 數據類型 變量名[= 初值表達式],…;
[]表示可以省略。
說明:
- 自動變量是局部變量。自動變量的作用域僅限於定義該變量的個體內。在函數中定義的自動變量在函數內有效,複合語句中的在複合語句中有效
- 自動變量屬於動態存儲,使用時,定義該變量的函數被調用才分配存儲單元,函數調用結束,釋放存儲單元。函數調用結束後,自動變量的值不能保留
- 自動變量的作用域和生存期都侷限於定義它的個體內(函數和複合語句內);同名變量不會混淆
- 使用未賦初值的自動變量
#include "stdio.h"
main()
{
int x=1;
{
int prt(); //聲明
int x=3;
prt();
printf("2nd x=%d\n",x);
}
printf("1st x=%d\n",x);
return 0;
}
int prt()
{
int x=5;
printf("3th x=%d\n",x);
return 0;
}
6.5.2 寄存變量(register)
寄存器變量具有與自動變量完全相同的性質。爲了提高效率,C語言允許將局部變量的值放在CPU寄存器中,這種變量叫“寄存器變量”,用關鍵字register表示。
例:求階層
說明:
只有局部自動變量和形式參數可以作爲寄存器變量
一個計算機系統中的寄存器數目有限,不能定義任意多個寄存器變量
#include "stdio.h"
main()
{
int fac(int n);
int i;
for(i=0;i<=5;i++)
{
printf("%d!=%d\n",i,fac(i));
}
return 0;
}
int fac(int n) //將實參i給形參n
{
register int i,f=1; //形參-由於頻繁使用變量i,故將它放在寄存器中
for(i=1;i<=n;i++)
{
f=f*i;
}
return (f);
}
6.5.3 靜態變量(static)
函數中的局部變量的值在函數調用結束後不消失而保留原值,這時就應該指定局部變量爲“靜態局部變量”,關鍵字static聲明
格式:
static 數據類型 變量名[=初始化常量表達式].…;
說明:
- 靜態變量的存儲空間在程序的整個運行期間是固定的。一個變量被指定爲靜態,在編譯時就爲其分配存儲空間,程序一開始執行便被建立,直到該程序執行結束都是存在的
- 靜態變量的初始化是在編譯時進行,在定義時只能使用常量或常量表達式進行顯示初始化。未顯示初始化時,編譯時將它們初始化爲0(int)或0.0(float)
自動變量沒有初始化的問題,只有靜態變量和外部變量有初始化問題。
對自動變量稱爲“賦初值”
對靜態變量和外部變量稱爲“初始化”
- 函數多次被調用的過程中,靜態局部變量的值具有可繼承性
#include "stdio.h"
main()
{
int f(int a);
int a=2,i;
for(i=0;i<3;i++)
{
printf("%d\n",f(a));
}
return 0;
}
int f(int a)
{
auto int b=0;
static int c=3; //值可以繼承;靜態局部變量
b=b+1;
c=c+1;
return(a+b+c);
}
6.5.4 外部變量
外部變量即爲全局變量是在函數的外部定義的,其作用域爲從變量定義處開始,到程序文件末尾。
如果外部變量不在文件的開頭定義,其有效的作用範圍只限於定義處到文件的末尾。
如果在定義點之前的函數想引用外部變量,則應該在引用之前用關鍵字extern對該變量作“外部變量聲明”,表示該變量是應該已經定義的外部變量。
例:外部變量代碼
- 可將外部變量的作用域擴充到其他文件,這時在需要用到這些外部變量的文件中,對變量用extern作聲明即可。
- 限定本文件的外部變量只在本文件中使用。如果有的外部變量只允許本文件使用而不允許其他文件使用,則可以在此外部變量前加一個static,使其有限局部化,稱爲靜態外部變量
#include "stdio.h"
main()
{
int mymax(int x,int y);
extern a,b; //外部變量聲明從而合法的引用
printf("%d\n",mymax(a,b));
return 0;
}
int mymax(int x,int y)
{
int z;
z=x>y?x:y;
return (z);
}
int a=13,b=1;
//在本程序中最後一行定義了外部變量a,b;外部變量定義的位置在main函數之後
6.6 編譯預處理
編譯預處理是在編譯前對源程序進行的一些預處理。預處理由編譯系統中的預處理程序按源程序中的預處理命令進行。
C語言預處理命令均已“#”開頭,末尾不加分號,以來區別C語句
可出現在程序中的任何位置,作用域是自出現點到所在源程序的末尾。如:#define和#include
6.6.1 宏定義
C語言允許用一個標識符來表示一個字符串,稱爲“宏”。
被宏定義的標識符稱爲“宏名”。
在編譯預處理時,對程序中出現的“宏名”。都用宏定義中的字符串去代換,這稱爲“宏代換”、“宏展開”
宏定義是由源程序中的宏定義命令完成的,宏代換是預處理程序自動完成的。
“宏”分爲有參數和無參數兩類。
1.無參數宏定義
無參宏的宏名後不帶參數,一般形式爲:#define 宏名 字符串
宏名的標識符習慣上用有意義且容易理解的大寫字母來表示。
“字符串”可以是常數、表達式、格式串等
一般寫在文件開頭函數體的外面,有效範圍是從定義宏命令之後到遇到終止宏命令#undef爲止,否則其作用域將一直到源文件結束。
例:#define PI 3.14
定義了宏名PI代表3.14,在預編譯處理時,系統將把該命令之後作用之內的所有PI都自動用3.14代換,即進行宏展開。
減少字符串的重複書寫、修改重複使用的字符串的工作變得簡單
注意:
- 宏定義是用宏名來表示一個字符串,在宏展開時又以該字符串取代宏名,這只是一個簡單的替換,字符串中可以含任何字符,常數、表達式,預處理程序對它不作任何檢查,如有錯誤,只能在編譯已被宏展開後的源程序時所發現。
- 如果在一行中寫不下整個宏定義,需要用兩行或更多行來書寫時,只需要在每行的最後一個字符的後面加上反斜槓“\”,並在下一行的最開始接着寫即可
- 宏定義必須寫在函數之外,其作用域爲宏定義命令起到源程序結束。如要終止其作用域可使用#undef命令
- 宏名在源程序中若用引號括起來,則預處理程序不對其作宏代換
例:printf("PI=",PI);
在預處理時,將只對第二個PI進行代換,對第一個雙引號中的PI,系統不對其作代換
- 宏定義允許嵌套,在宏定義的字符串中可以使用已經定義的宏名。在宏展開時,由預處理程序層層代換
例子:
#define R 5.6
#define PI 3.14
#define L 2*PI*R
#define S PI*R*R //PI、R是已經定義的宏名
2.帶參數宏定義
C語言允許宏帶有參數。在宏定義中的參數稱爲形式參數,在宏調用中的參數稱爲實際參數。
參數宏:調用中,不僅要進行宏展開,而且要用實參去代換形參
形式:
#define 宏名(形參表列) 字符串
在字符串中含有各個形參
帶參宏調用的一般形式爲:
宏名(實參表列);
例如:
#define M(y) y*y+3*y //宏定義
k=M(5) //宏調用
例:求兩個數中較大者
#include "stdio.h"
#define MAX(a,b)(a>b)?a:b
main()
{
int x,y,mymax;
printf("請輸入兩個數:");
scanf("%d %d",&x,&y);
mymax=MAX(x,y); //宏調用
printf("max=%d\n",mymax);
return 0;
}
- 帶參宏定義中,宏名和形參表之間不能有空格出現
- 在帶參數宏定義中,形式參數不分配內存單元,因此不必作類型定義。而宏調用中的實參有具體的值,要用它們去代換形參,因此必須作類型說明,與函數的情況不同。函數調用中,形參與實參是兩個不同的量,作用域不同,存在值傳遞;而帶宏參數中只是符號代換,不存在值傳遞的問題。
- 宏定義中形參是標識符,宏調用中的實參可以是表達式
- 宏定義中,字符串內的形參通常要用括號括起來以避免出錯。除了將宏定義字符串中的參數都用括號括起來,還需要將整個字符串部分也用括號括起來。
- 同一表達式用函數處理的結果與宏定義處理的結果不一定相同
例如:求函數n的平方
#include "stdio.h"
#define SQ(y)((y)*(y))
main()
{
int i=1;
while(i<5)
{
printf("%d\n",SQ(i++));
}
}
6.6.2 文件包含
文件包含是C語言預處理程序的另一個重要功能。
形式:
#include "文件名"
例如:
#include "stdio.h"
文件包含命令的功能就是把指定的文件插入該命令位置取代該命令,從而把指定的文件和當前的源程序文件連成一個源文件。
說明:
- 文件包含命令中的文件名可以用雙引號括起來,也可以用尖括號括起來
尖括號:表示在包含文件目錄中查找(包含目錄是由用戶在設置環境時設置的),而不再源文件目錄查找;
雙引號:首先在當前的源文件目錄中查找,若未找到纔到包含目錄中查找
- 一個include命令只能指定一個被包含文件,若有多個文件要包含,則需用多個include命令
- 文件包含允許嵌套,即在一個被包含的文件中又可以包含另一個文件
預處理程序提供了條件編譯的功能。可以按不同的條件按去編譯不同的程序部分,因而產生不同的目標代碼文件。
1)形式1:
#ifdef 標識符
程序段1
#else
程序段2
#endif
標識符是指已用宏命令#define定義的宏名,而程序段可以是編譯預處理命令行,也可以是C語言組。如果標識符已被#define命令定義過則對程序段1進行編譯;否則對程序段2進行編譯。
#include "stdio.h"
#define REAL float
main()
{
#ifdef REAL
REAL a;
printf("輸入一個實數: ");
scanf("%f",&a);
printf("這個實數是:%f\n",a);
#else
float a;
printf("輸入一個單精度浮點數: ");
scanf("%f",&a);
printf("這個單精度浮點數是:%f\n",a);
#endif
}
2)形式2:
#ifndef 標識符
程序段1
#else
程序段2
#endif
功能:如果標識符未被#define命令定義過則對程序段1進行編譯,否則對程序段2進行編譯。
3)形式3:
#if 表達式
程序段1
#else
程序段2
#endif
功能:如果表達式的值爲真(非0),則對程序段1進行編譯,否則對程序段2進行編譯。
6.7 應用舉例
例子:編寫一個程序完成“菜單”功能:提供三種選擇路徑。
代碼太長,望讀者諒解;關於C代碼實例可進我主頁下載。嘻嘻嘻嘻嘻~~~~
謝謝各位~~~~
print—是函數,可以返回一個值,只能有一個參數;
println—與print唯一的區別是println換行輸出;
printf—函數,把文字格式化後輸出,直接調用系統調用進行IO,非緩衝