函數
定義:本質上是一段可以被連續調用、功能相對獨立的程序段
c語言是通過“函數”實現模塊化的。根據分類標準不同函數分為以下幾類。
用戶角度:庫函數、自定義函數
函數形式:有參函數、無參函數
作用域:外部函數、內部函數
-
函數的引入實現了兩個目的:1.結構化、模塊化編程 2.解決代碼重復
-
函數為什么要先定義后使用:
通知編譯系統函數的返回值類型、函數的名稱、函數的參數個數與類型以及函數實現什么功能。
-
編譯系統不檢查函數名與形參名同名,即允許函數名與形參名同名。
-
函數是c語言主要組成部分,是模塊化程序設計的思路。
-
一個c程序由一個或多個源程序文件組成,一個源程序文件對應一個程序模塊。一個源程序文件(程序模塊)由一個或多個函數及其他有關內容(指令、數據聲明與定義等)組成,一個源程序文件是一個編譯單位。
-
函數由函數原型與函數體構成,函數體由聲明部分與語句部分構成。聲明部分由函數內的變量定義以及對其他函數的聲明構成
-
c程序的執行總是從main函數開始的,也是從main函數結束的。
-
函數返回值的類型應該與函數類型一致,若不一致以函數類型為準,即函數類型決定返回值類型。
-
函數間可以相互調用,但不能調用main函數(main只能被操作系統調用)
-
函數通常先定義,再聲明,再使用。
-
對于不帶返回值的函數,應該定義為void類型,此時函數體內的return語句不應該攜帶返回值。
-
函數的形參只能是默認的auto類型,即不能自己加auto或者其它類型
-
c語言規定全局變量只允許使用常量或常量表達式初始化
#include <stdio.h> int a=12,b=13+2; int c=a+b;//這里編譯報錯,因為a、b屬于變量 int main(){}
-
全局變量不允許與符號常量同名
#include <stdio.h> float A=11.2;//全局變量雖然在符號常量前面,但是會先進行預編譯進行宏替換,所以這里實際是 int 1.34=11.2,顯然是一個語法錯誤 #define A 1.34 ? #define B 100 int B=100;//全局變量在符號常量后面,這里會產生編譯錯誤,因為這里實際是 int 1.34=11.2,顯然是一個語法錯誤
-
extern一般都是在其他文件中引入本文件的全局變量或函數在其他文件使用,或者擴展本文件全局變量的作用域時才會在本文件使用
-
默認定義的全局變量與函數是可以在同程序的文件之間共享,只是需要extern聲明進來,所以這也是同一程序不同c文件不允許出現同名的外部變量與外部函數
-
課本要求函數實參的個數、類型必須與形參一致,否則會出現編譯錯誤。但實際中實參的類型可以與形參不一致(地址除外)
函數定義
函數與變量、數組一樣,都必須先定義后使用。函數的定義包括:指定函數數據類型(函數的返回值)、指定函數名(函數名反應其功能)、指定形參的類型與名字、完成函數功能。函數定義的目的是為了通知編譯系統函數的返回值類型、函數的名稱、函數的參數個數與類型以及函數實現什么功能。
格式:
1.定義無參函數: ?數據類型 函數名(void){ //形參可以為空也可以寫void,表示沒有參數函數體;}2.定義有參函數: ?數據類型 函數名(形參列表){函數體;}? 3.定義空函數:void 函數名(){//沒有函數體}
-
空函數的作用:給以后編寫的函數占據一個位置
-
函數體包括聲明部分與語句部分(執行部分)
-
函數定義時指定的形參不占用存儲空間,只有在調用時才分配存儲空間
-
函數定義省略函數的數據類型,此時默認為int類型,但是注意在聲明時必須加上int,否則會編譯錯誤
int main() {int sum(int,int);//但是聲明時必須指定為intreturn 0; } sum(int sum,int y){//省略了函數數據類型return 0; }
-
c語言不允許變量名與函數名相同,但是允許形參名與函數名相同(不建議)
int sum(int sum,int y){return 0; }
-
形參與實參可以同名,因為他們的作用域不同,會分配不同的內存空間(不建議)
int add(int x);//add函數聲明 int x=12; add(x);//實參x與形參x同名,但是不影響
-
函數定義的目的:通知編譯系統函數的返回值類型,函數名、函數的參數個數及類型以及函數的功能。
-
c語言函數不允許嵌套定義(在一個函數內定義另一個函數),即所有函數都是平行的。(但是允許嵌套調用)
函數聲明
格式:
main(){add(int x);//add函數聲明sum(int);//sum函數聲明(可以不寫參數名) } void add(int x){} void sum(int y){}
作用:告訴計算機次函數在后面已經定義,將函數的作用域提升到聲明處,在調用函數檢查函數的正確性
-
函數聲明就是函數原型(函數首部)加一個分號即可
-
函數聲明中可以省略參數名而只寫參數數據類型,因為函數調用不會檢查參數的名字
-
函數調用時會檢查函數類型,函數名,參數類型、個數,順序必須與函數聲明一致
-
可以將被調函數的聲明放在文件開頭(所有函數之前)進行聲明(又稱為外部聲明),這樣所有函數都可以不在本函數內聲明直接調用該函數
#include <stdio.h> int add(int);//add函數聲明 int main(){add(1);//調用時可直接調用 } int add(int x){return 0;};//add函數定義
-
如果被調函數在主調函數上面定義,那么可以不進行函數聲明。
int sum(int x,int y){return 0; } int main() {int add=sum(2,2);//add函數已經在上面定義,故可不聲明直接調用printf("%d\n",add);return 0; }
-
將一個底部的函數聲明到另一個函數內,該函數的作用域只是增加了這一個函數的范圍,哪怕中間還有其他函數,也無法調用該函數
在一個函數內調用另一個函數需要滿足的條件:
-
如果是庫函數,則需要在開頭使用預處理命令對這些庫函數引入
-
如果是自定義函數需要對該函數定義,然后在主調函數中對被調函數作函數聲明
函數調用
格式:
函數名(實參列表);//如果是無參函數,則實慘列表為空,但是不能省略括號
-
實參列表如果是多個使用逗號隔開,無參函數調用實參列表為空,但是不能省略括號
-
函數調用有3種方式:函數調用語句、函數表達式、函數嵌套調用。
printf("hello!");//函數直接調用 SUM=sum();//將函數的返回值賦值給一個變量或出現在其他表達式中參加運算 sum(a,sum());//將返回值作為其他函數參數帶入運算
-
實參可以是常量、變量、表達式。
函數調用的過程:
-
函數開始調用:為形參分配空間大小,然后將實參的值傳遞給形參
-
函數開始執行:為函數內定義的局部變量分配空間(函數內的static變量已經在編譯階段就開始初始化并分配空間),并對需要調用的其他函數進行聲明等,再依次執行其他語句,直到第一個return結束
-
函數調用結束:釋放本函數內局部變量的空間(函數內的static變量不會被釋放),返回主調函數繼續執行。
實參與形參之間的數據傳遞:
-
形參于實參之間的數據傳遞也稱為虛實結合
-
實參可以是常量、變量、表達式,調用時實參的數據類型與個數必須與形參一一對應。如果實參的數據類型與形參不一致,則會進行自動類型轉換,即把實參數據轉換為形參類型再賦值。若實參于形參個數不一致則會出現編譯錯誤。
int sum(int a);//函數已定義,在這里聲明 SUM=sum(1.34);//將1.34轉換為int類型,即1再賦值給形參a
-
按值傳遞:形參的改變不會影響實參的值。(數據只能單向由實參—>形參)
按地址傳遞:改變形參指向地址的值會改變實參指向地址的值,但改變形參本身的地址值不會改變實參本身的地址值。
int main(){void fn(int *,int *);int a=12,b=20,*p=&a;fn(p,&b);printf("%d\t%d\n",a,*p);/*輸出:10,10(可見在函數中改變指向地址的值會改變外面實參地址的值,但是改變指針本身指向不會改變外面指針的指向)*/ } void fn(int *p,int *n){*p=10;p=n; }
函數的返回值:
-
return后面的括號可以加也可以不加。例如 return(z)等價于return z。return后面的值可以是一個表達式。例如 return x+y
-
函數為void類型時函數體內return不應該帶返回值。
-
函數的類型與return不一致時,以函數類型為準,此時數據類型不同會進行自動類型轉換。即函數類型決定返回值類型。
-
有數據類型的函數省略return時函數會返回一個不確定的值。
函數的嵌套調用
定義:在一個函數內調用另一個函數
函數的遞歸調用
定義:函數直接或間接調用自己本身(直接遞歸調用、間接遞歸調用)
遞歸調用的條件:
-
要解決的問題能能簡化為一個新問題,這個新問題的解決方法與原解決問題的方法相同--尋找遞歸表達式
-
要有一個明確的結束遞歸條件結束遞歸調用--尋找遞歸出口
遞歸調用分為兩個階段:
-
遞推階段
-
回歸階段
數組作函數參數
1.數組元素作函數參數
定義:數組元素只能作函數實參,不能用作形參。數組元素作為實參傳值給形參時是按值傳遞的方式,即數據從實參單向傳遞給形參。
void fn(int a[2]){}//a[2]是一個具有2個元素數組,而并非數組元素
普通變量與地址做函數參數的不同:
-
前者按值傳遞、后者按地址傳遞
-
前者分配與實參相應的存儲空間、后者只分配第一保存地址的空間
-
前者不回改變實參的值,后者會改變實參的值。
2.數組名作函數參數
定義:數組名可以做形參也可以做實參,數組名作實參傳遞的是數組的首地址。
-
數組名在做函數形參時,不管是函數定義還是函數聲明,都是作為指針變量處理,調用函數傳入的實際上也是地址,而并非整個數組
int main(){int fn(int *a);//a實際是一個指針變量,可以看見聲明int *a與下面的函數定義的int a[]等價int a[]={12,13,15};printf("%d\n",fn(a));//13int b=100;printf("%d\n",fn(&b));//100(這里也可以看出c系統對數組做形參實際按指針處理,否則這里不能傳入變量b的地址) } int fn(int a[]){if(*a==100)return 100;return *(++a);//可以看見這里可以對數組名進行指針運算 }
-
數組名作函數參數,必須在主調函數與被調函數分別定義數組
int abc(int arr[]){//被調函數數組arr} int main(){int arr2[10]={};//主調函數數組arr2abc(arr2);//將arr2作為函數參數傳遞給abc函數的形參arr }
-
數組做函數參數時實參類型必須與形參類型一致,否則會出現編譯錯誤(普通變量做函數參數兩者不同時會進行自動轉換)
即按值傳遞可以類型不同,會進行自動類型轉換。按地址專遞傳遞則必須類型相同。
-
形參數組如果是一維數組,那么可以省略長度。多維素組只能省略最低維(c編譯系統不會檢查第1維的大小)
int arr(int abs[]){}//形參是一維數組,可以參略長度 int arr(int abs[][2]){}//形參是二維數組,只可參略第一維長度 int arr(int abs[3][2]){}//與上面等價 int arr(int abs[4][2]){}//形參數組的第一維在與實參數組相同的情況下,可大于實參數組的第1維,但不能小。例如實參a[2][2]
-
sizeof(形參數組)返回的是實參數組的首地址,也就是首地址指針的sizeof
#include <stdio.h> int aver(int arr[]){return sizeof(arr);//實際上是arr數組a[0]的空間大小,是一個指針所以為8 } int main(){int arr[10];int length=aver(arr);printf("%d\n",length);//8printf("%d\n",sizeof(arr));//40(arr在本函數中任然屬于一個變量,sizeof返回整個數組的空間大小printf("%d\n",sizeof(arr[0]));//4(arr第一個元素在本函數中也相當于一個變量,返回4)}
-
數組名作為函數實參,函數聲明時,不允許值只寫數據類型,正確形式為:數據類型 []
int A=12; int main(){int add(int[]);//不能是 int add(int) } int add(int a[]){}
局部變量與全局變量
任何變量都有作用域,根據作用域不同分為局部變量與全局變量。(數組、函數、指針等其實本質上都等同于變量,其也有作用域)
1.局部變量:函數內定義的變量
分類:在函數內定義、復合語句中定義、函數的形參
-
函數內定義的局部變量只在本函數內有效
-
復合語句的局部變量只在復合語句內有效(范圍更小)
-
函數的形參只在本函數內有效
-
不同函數內的局部變量可以重名互不干擾。同一函數內不同作用域內的函數也可以重名,例如在復合語句定義的變量可以在包含該復合語句的函數中其他位置定義同名變量,在調用時候采取就近原則
-
c語言不允許函數名與變量名相同,但是允許在本函數中定義的局部變量與本函數重名
int add(){int add=20;return add; }
2.全局變量:函數外定義的變量
-
全局變量通常可以為本文件中其他所有函數共用,有效范圍為定義位置到本源文件結束。(
-
全局變量的第1個字母通常大寫
-
全局變量與局部變量重名以就近原則調用
-
默認定義的全局變量不加static的話可以被其他源程序文件調用(但是要使用extern要進行外部聲明),所以相關聯的多個.c文件的程序中,不同的c文件不允許有同名的全局變量,除非同名的全局變量屬于靜態外部變量
為什么不建議過多的使用全局變量?
-
全局變量在整個程序運行期間都占有存儲單元
-
全局變量會使函數的通用性降低
-
全局變量過多,會降低程序的清晰性
變量的存儲方式與生命期
1.存儲方式
分類:靜態存儲、動態存儲
-
全局變量全部采用的靜態存儲(包括靜態局部變量),他們都是在程序開始執行時(編譯)就分配固定的內存空間。
-
局部變量全部采用動態存儲,只有在函數調用時分配空間,調用結束后就銷毀
??注意:全局變量與靜態局部變量都在編譯階段就分配了內存地址,但是全局變量是在編譯階段就進行了初始化,而靜態局部變量是在程序運行到該函數時才進行初始化,但是其內存空間是在編譯階段分配好了的。
2.存儲類別:
每一個變量(包括數組、函數、指針變量)在定義時都要指定兩個屬性:數據類型、存儲類別。
分類:auto、static、register、extern
??注意:自動變量默認為auto,全局變量默認為extern,其可以在多個文件之間共享,但并非代表其作用域在所有文件,其作用域仍然只限于定義處到本文件結束,其他文件想使用該全局變量需要使用extern聲明。換句話說,全局變量在其定義的文件內是全局可見的,但在其他文件中則不是默認可見的。為了在其他文件中使用同一個全局變量,需要使用 extern
關鍵字來聲明該變量,這樣編譯器就知道在其他地方已經定義了該變量,并且可以在當前文件中訪問它。
局部變量的存儲類別:
-
static(靜態局部變量):使用static關鍵字聲明的局部變量(static變量與全局變量一樣在編譯開始時就分配了存儲單元,直到程序結束。但是static變量的作用域也僅限于本函數內或復合語句內)
-
對于沒有初始化的局部變量,靜態局部變量(全局變量也是)系統自動賦初值0、0.0、'\0',而自動變量的值確實不可預知的。
-
register(寄存器變量):為提高執行效率,把局部變量的值放在CPU的寄存器中再進行頻繁運算(因為寄存器的存取>內存的存取)
-
auto、static、register屬于局部變量的存儲類別,一般只能用于局部變量(static還可以放在全局變量前)
全局變量的存儲類別:
-
全局變量的作用域是定義處到源文件結束,這就造成定義之前的函數無法訪問該全局變量,可以使用extern做外部變量聲明正常引用
#include <stdio.h> int main(){extern int A;printf("%d\n",A); } int A=12;
注意:將一個底部的全局變量外部聲明到另一個函數內,該全局變量的作用域只是增加了這一個函數的范圍,哪怕中間還有其他函數,也無法訪問該全局變量
#include <stdio.h> //extern int A;必須把外部聲明放在這里,才能把該全局變量作用域提高到全局,以下所有的函數都可以訪問 int a();//函數聲明 int main(){extern int A;//外部聲明printf("%d\n",A);//12// printf("%d\n",a());//這里依然無法訪問A,因為上面只是把A聲明到本函數內,而a函數沒有被聲明 } int a(){return A; } int A=12;
extern 數據類型 變量名可以省略為 external 變量名
extern int A,B,C 等價于 extern A,B,C
-
還可以使用extern將全局擴展到其他.c文件
文件file1.c #include <stdio.h> int A=12; int main(){ ? } ? 文件file2.c extern A;//此時在file2中可以訪問到A int main(){ ? } ? ??注意:使用 extern 關鍵字在其他文件中聲明全局變量時,不會為該變量分配新的內存空間,而是告訴編譯器該變量已經在其他地方定義了。
-
還可以使用static將全局變量限制在本文件中,其他文件不允許訪問(靜態外部變量)
文件file1.c #include <stdio.h> static int A=12; int main(){ ? } ? 文件file2.c extern A;//因為A被限制在file1.c中,所以這里的聲明無效 int main(){ ? }
聲明全局變量的存儲類型于聲明局部變量的存儲類型的區別:
-
對于局部變量使用auto、static、register聲明的局部變量是為了指定變量的存儲區域與生存期
-
對于全局變量使用extern與static是為了擴展或限制全局變量的作用域
內部函數與外部函數
內部函數:
定義:函數只能被本文件中的其他函數調用
格式:
static 數據類型 函數名(參數列表); 例如 static add(int a){}
外部函數:
定義:函數可以被其他文件中的函數調用
格式:
extern 數據類型 函數名(參數列表); 例如 extern add(int a);//聲明函數已外部定義
-
定義函數時省略存儲類別函數默認是外部函數
-
不加static定義的函數都屬于外部函數,所以多個c文件的程序在連接時,不允許出現同名的函數,否則會產生重復定義,除非同名的函數是內部函數
-
若要調用其他文件的函數,與變量一樣,需要使用extern將函數聲明到本文件中
總結:不管是全局的變量、數組、還是函數,都可以使用extern和static對作用域進行擴展或限制,而對于局部的變量、數組、函數來說使用auto、static等只是改變其存儲類型。
#include <stdio.h> extern int ARR[12]; // int a();//函數聲明 int main(){extern int a();//與上面的函數聲明等價printf("%d\n",a());//12printf("%d\n",ARR[0]);//1 } int a(){return 12; } int ARR[12]={1};