自定義函數和結構體
- 格式
- 定義函數
返回類型 函數名(參數列表){函數體}
其中函數體的最後一條語句應該是return 表達式;
,如果函數無需返回值,則返回類型應寫成void。
main函數也是有返回值的,返回0代表“正常結束”。 - 定義結構體
- 方法一
struct 結構體名稱{ 域定義 };
- 方法二
typedef struct { 域定義; }類型名;
此方法可使結構體像原生數據類型一樣使用這個自定義類型。
- 方法一
- 定義函數
- 計算組合數
編寫函數,參數是兩個非負整數n和m,返回組合數=,其中。例如,n=25,m=12時答案爲5200300。
注意函數的定義一定要定義在main函數之前,因爲程序在進行編譯時,從上到下進行編譯,如果先編譯main函數,就會識別不出調用的函數名。#include<stdio.h> // 函數定義 long long C(int n, int m){ // 保證m爲大的那個數 if(m < n-m){ m = n-m; } long long ans = 1; // 先約分數較大的階乘 for(int i=m+1; i <= n; i++){ ans *= i; } // 再除以剩下數的階乘 for(int i =1; i<= n-m; i++){ ans /= i; } return ans; } int main() { int n, m; scanf("%d%d", &n, &m); printf("%d", C(n, m)); return 0; }
- 素數判定
編寫函數。參數是一個正整數n,如果它是素數,返回1,否則返回0。#include<stdio.h> #include<math.h> // 判斷素數 int is_prime(int n) { // 1 是素數 if(n <= 1) return 0; // 開方,四捨五入 int m = floor(sqrt(n)+0.5); // 判斷 for(int i=2; i<= m; i++){ if(n%i == 0){ return 0; } } return 1; } // 主函數 int main() { int n; scanf("%d", &n); if(is_prime(n)){ printf("1"); }else{ printf("0"); } return 0; }
函數調用與參數傳遞
-
形參與實參
函數的形參和在該函數裏定義的變量都稱爲該函數的局部變量。不同函數的局部變量相互獨立,即無法訪問其他函數的局部變量。需要注意的是,局部變量的存儲空間是臨時分配的,函數執行完畢時,局部變量的空間將被釋放,其中的值無法保留到下次使用。與此對應的是全局變量。 -
調用棧(gdb調試)
- 調用棧描述的是函數之間的調用關係。它由多個棧幀組成,每個棧幀對應着一個未運行完的函數。棧幀中保存了該函數的返回地址和局部變量。
- 配置gdb環境變量
DevC++自帶gdb.exe,在安裝目錄中找到路徑,如E:\Dev-Cpp\MinGW64\bin
,添加到path中。 - 調試棧
-
用指針作參數
- 用函數交換變量
變量名前面加“&”得到的是該變量的地址。#include<stdio.h> void swap(int* a, int* b) { int t = *a; *a = *b; *b = t; } int main() { int a = 3, b = 4; swap(&a, &b); printf("%d %d\n", a, b); return 0; }
*a是指“a指向的變量”,而不僅僅是“a指向的變量所擁有的值”。
&
取地址運算符,*
取內容運算符。
- 用函數交換變量
-
初學者易犯錯誤
void swap(int* a, int* b){ int *t; *t = *a; *a = *b; *b = *t; }
這個程序錯在哪裏?
t是一個指向int型指針,因此*t是一個整數。
用一個整數作爲輔助變量去交換兩個整數有何不妥?
事實上,如果用這個函數去替換上面的程序,很可能會得到“4 3”的正確結果。
問題在於,t存儲的地址是什麼?也就是說t指向哪裏?因爲t是一個變量(指針也是一個變量,只不過類型是“指針”),所以根據規則,它在賦值之前是不確定的。如果這個“不確定的值”所代表的內存單元恰好是能寫入的,那麼這段程序將正常工作;但如果它是隻讀的,程序可能崩潰。 -
數組作爲參數和返回值
如何把數組作爲參數傳遞給函數?int sum(int a[]) { int ans = 0; for(int i = 0; i < sizeof(a); i++){ ans += a[i]; } return ans; }
上面的函數是錯誤的,因爲sizeof(a)無法得到數組的大小。爲什麼會這樣?
因爲把數組作爲參數傳遞給函數時,實際上只有數組的首地址作爲指針傳遞給了函數。- 計算數組的元素和
以數組爲參數調用函數時,實際上只有數組首地址傳遞給了函數,需要令加一個參數表示元素個數。除了把數組首地址本身作爲實參外,還可以利用指針加減法把其他元素的首地址傳遞給函數。int sum(int* a, int* b){ int ans = 0; for(int i = 0; i< n; i++){ ans += a[i]; } return ans; }
一般的,若p是指針,k是正整數,則p+k就是指針p後面第k個元素,p-k是p前面的第k個元素,而如果p1和p2是類型相同的指針,則p2-p1是從p1到p2的元素個數(不含p2)。 - 計算左閉右開區間內的元素和
1. 方法一 #include<stdio.h> #include<math.h> int sum(int* begin, int* end){ int n = end - begin; int ans = 0; for(int i = 0; i < n; i++){ ans += begin[i]; } return ans; } int main() { int a[] = {1, 2, 3, 4, 5}; printf("%d \n", sum(a, &a[3])); return 0; } 2. 方法二 #include<stdio.h> #include<math.h> int sum(int* begin, int* end){ int *p = begin; int ans = 0; for(int *p = begin; p!= end; p++){ ans += *p; } return ans; } int main() { int a[] = {1, 2, 3, 4, 5}; printf("%d \n", sum(a, &a[3])); return 0; }
- 計算數組的元素和
遞歸
- 遞歸定義
遞歸的定義如下:
遞歸: 參見“遞歸”
原來遞歸就是“自己用到自己”的意思。 - 遞歸函數
數學函數也可以遞歸定義。例如,階乘函數可以定義爲:
對應程序如下:
C語言支持遞歸,即函數可以直接或間接地調用自己。但要注意爲遞歸函數編寫終止條件,否則將產生無限遞歸。#include<stdio.h> int f(int n){ return n == 0 ? 1: f(n-1)*n; } int main() { printf("%d\n",f(3)); return 0; }
- C語言對遞歸的支持
- 由於使用了調用棧,C語言支持遞歸。在C語言中,調用自己和調用其他函數沒有本質區別。
- 段錯誤與棧溢出
- “段”是指二進制文件內的區域。
- 在gcc生成的可執行文件中,正文段(Text Segment) 用於存儲指令,數據段(DataSegment)用於存儲已初始化的局部變量,BSS段(BSS Segment)用於存儲未賦值的全局變量所需的空間。
- 每次遞歸調用都需要往調用棧裏增加一個棧幀,久而久之就越界了。這種情況叫做棧溢出。
- 調用棧並不儲存在可執行文件中,而是在運行時,程序會動態創建一個堆棧段,裏面存放着調用棧,因此保存着函數的調用關係和局部變量。
- 那麼棧空間究竟有多大?
在Linux中,棧大小並沒有存儲在可執行程序中,只能用ulimit命令修改;在windows中,棧大小存儲在可執行程序中,用gcc編譯時可以通過-W1,--stack=<byte count>
指定。 - 現在理解了在介紹數組時,建議“把較大的數組放在main函數外”,是因爲,局部變量也是放在堆棧段的。棧溢出不一定是遞歸調用太多,也可能是局部變量太大。只要總大小超過了允許的範圍,就會產生溢出。