4. 函數和遞歸

自定義函數和結構體

  1. 格式
    • 定義函數
      返回類型 函數名(參數列表){函數體}
      其中函數體的最後一條語句應該是return 表達式;,如果函數無需返回值,則返回類型應寫成void。
      main函數也是有返回值的,返回0代表“正常結束”。
    • 定義結構體
      1. 方法一
        struct 結構體名稱{ 域定義 };
      2. 方法二
        typedef struct { 域定義; }類型名;
        此方法可使結構體像原生數據類型一樣使用這個自定義類型。
  2. 計算組合數
    編寫函數,參數是兩個非負整數n和m,返回組合數CnmC^m_{n}=n!m!(nm)!n!\over m!(n-m)!,其中mn25m \leq n \leq 25。例如,n=25,m=12時答案爲5200300。
    #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;
    }
    
    注意函數的定義一定要定義在main函數之前,因爲程序在進行編譯時,從上到下進行編譯,如果先編譯main函數,就會識別不出調用的函數名。
  3. 素數判定
    編寫函數。參數是一個正整數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;
    }
    

函數調用與參數傳遞

  1. 形參與實參
    函數的形參和在該函數裏定義的變量都稱爲該函數的局部變量。不同函數的局部變量相互獨立,即無法訪問其他函數的局部變量。需要注意的是,局部變量的存儲空間是臨時分配的,函數執行完畢時,局部變量的空間將被釋放,其中的值無法保留到下次使用。與此對應的是全局變量。

  2. 調用棧(gdb調試)

    • 調用棧描述的是函數之間的調用關係。它由多個棧幀組成,每個棧幀對應着一個未運行完的函數。棧幀中保存了該函數的返回地址和局部變量。
    • 配置gdb環境變量
      DevC++自帶gdb.exe,在安裝目錄中找到路徑,如E:\Dev-Cpp\MinGW64\bin,添加到path中。
    • 調試棧

  3. 用指針作參數

    • 用函數交換變量
        #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指向的變量所擁有的值”。
      &取地址運算符, *取內容運算符。
  4. 初學者易犯錯誤

    void swap(int* a, int* b){
        int *t;
        *t = *a;
        *a = *b;
        *b = *t;
    }
    

    這個程序錯在哪裏?
    t是一個指向int型指針,因此*t是一個整數。
    用一個整數作爲輔助變量去交換兩個整數有何不妥?
    事實上,如果用這個函數去替換上面的程序,很可能會得到“4 3”的正確結果。
    問題在於,t存儲的地址是什麼?也就是說t指向哪裏?因爲t是一個變量(指針也是一個變量,只不過類型是“指針”),所以根據規則,它在賦值之前是不確定的。如果這個“不確定的值”所代表的內存單元恰好是能寫入的,那麼這段程序將正常工作;但如果它是隻讀的,程序可能崩潰。

  5. 數組作爲參數和返回值
    如何把數組作爲參數傳遞給函數?

    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;
      }
      

遞歸

  1. 遞歸定義
    遞歸的定義如下:
    遞歸: 參見“遞歸”
    原來遞歸就是“自己用到自己”的意思。
  2. 遞歸函數
    數學函數也可以遞歸定義。例如,階乘函數f(n)=n!f(n)=n!可以定義爲:
    {f(0)=1f(n)=f(n1)×n(n1) \begin{cases} f(0) = 1\\ f(n) = f(n-1) \times n(n \geq 1) \end{cases}
    對應程序如下:
    #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語言支持遞歸,即函數可以直接或間接地調用自己。但要注意爲遞歸函數編寫終止條件,否則將產生無限遞歸。
  3. C語言對遞歸的支持
    • 由於使用了調用棧,C語言支持遞歸。在C語言中,調用自己和調用其他函數沒有本質區別。
  4. 段錯誤與棧溢出
    • “段”是指二進制文件內的區域
    • 在gcc生成的可執行文件中,正文段(Text Segment) 用於存儲指令,數據段(DataSegment)用於存儲已初始化的局部變量,BSS段(BSS Segment)用於存儲未賦值的全局變量所需的空間。
    • 每次遞歸調用都需要往調用棧裏增加一個棧幀,久而久之就越界了。這種情況叫做棧溢出。
    • 調用棧並不儲存在可執行文件中,而是在運行時,程序會動態創建一個堆棧段,裏面存放着調用棧,因此保存着函數的調用關係和局部變量。
    • 那麼棧空間究竟有多大?
      在Linux中,棧大小並沒有存儲在可執行程序中,只能用ulimit命令修改;在windows中,棧大小存儲在可執行程序中,用gcc編譯時可以通過-W1,--stack=<byte count>指定。
    • 現在理解了在介紹數組時,建議“把較大的數組放在main函數外”,是因爲,局部變量也是放在堆棧段的。棧溢出不一定是遞歸調用太多,也可能是局部變量太大。只要總大小超過了允許的範圍,就會產生溢出。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章