首頁 > 軟體

C語言從零探索函數的知識

2022-04-21 16:00:32

一、初探程式中的函數

函數的概念

  • 函數是具有特定功能的程式部件(可當黑盒使用)
  • 函數有明確的使用方式(固定輸入對應固定輸出)
  • 函數在程式中可重複使用(程式中的工具)

函數的型別

  • 資料處理(資料→資料)

通過某種規則將 x處理成 y,如: y = 2x +1

  • 過程定義(資料→功能)

(根據資料)執行一系列動作,進而完成某種功能,如:螢幕列印

C語言中函數的組成部分

函數名:函數的唯一標識

函數引數定義:資料輸入(資料→資料,資料→動作)

函數返回型別:

  • 資料輸出(資料→資料)
  • 無返回值(資料→動作)

廣義函數範例:

返回型別 函數名(引數1,引數2)
{
    程式語句1;
    程式語句2;
    ......;
    程式語句n;
}

C語言中的函數範例

函數的呼叫

  • 通過函數名呼叫已經定義好的函數
  • 函數呼叫時需要依次指定函數引數的具體值
  • 函數呼叫的結果(返回值)可儲存在同型別的變數中

下面看一段函數呼叫的程式碼:

#include <stdio.h>
int func_demo( int x )
{
    int y = 0;
    y = 2 * x  - 1;
    return y;
}
int main()
{
    int r1 = func_demo(1);
    int r2 = func_demo(5);
    int r3 = func_demo(10);
    printf("r1 = %dn", r1);
    printf("r2 = %dn", r2);
    printf("r3 = %dn", r3);
    return 0;
}

下面為輸出結果:

下面再看一段編寫函數計算累加和的程式碼:

#include <stdio.h>
int sum (int n)
{
    int r = 0;
    int i = 0;
    for(i=1; i<=n; i++)
    {
        r += i;
    }
    return r;
}
int main()
{
    int o[10] = {10, 20, 30, 40, 50, 100};
    int r[10];
    int i = 0;
    for(i=0; i<10; i++)
    {
        r[i] = sum(o[i]);
    }
    for(i=0; i<10; i++)
    {
        printf("sum(%d) = %dn", o[i], r[i]);
    }
    return 0;
}

下面為輸出結果:

採用陣列可以便捷的求出從1加到指定的數。

小結

  • 函數是具有特定功能的程式部件
  • 函數由函數名,引數,返回型別以及函數體組成
  • 通過函數名呼叫已經定義好的函數,並同時傳入引數值
  • 函數的本質就是可重複利用的程式碼段

二、深入淺出函數呼叫

再論C語言程式的入口

  • 一般情況下,C語言程式從main()開始執行

深入理解main()

  • main() 是應用程式與作業系統的一個“約定”
  • 當作業系統執行應用程式時,首先呼叫的就是 main() 函數
  • 應用程式必須執行於作業系統,接受作業系統管理

應用程式的執行

應用程式執行流程

下面看一段程式碼,實際感受一下吧:

#include<stdio.h>
#include<stdlib.h>
int main()
{
    printf("Hello World!n");
    system("pause");
    return 0;
}

沒錯,就是這個簡單的不能再簡單的程式碼,開啟Test.exe,得到下圖:

下面來證明一下 返回值 0 是返回給作業系統,首先開啟命令提示字元,如下:

然後在命令提示字元上切換到這個目錄,採用 cd 命令,如下:

然後執行 Test.exe,如下:

按下回車,輸入echo %errorlevel%,如下:

可以看到輸出 0 ,這是 main 函數中的返回值。如果把 return 0那裡換成 return 666,那麼執行 echo %errorlevel% 會輸出 666。這就說明返回值成功返回給作業系統。

核心本質

  • C程式由一系列不同功能的函數構成
  • 函數之間通過相互呼叫組合“小功能”構成“大功能”
  • 整個C程式的功能由函數的組合呼叫完成

工具包的本質

  • 工具包就是函數集,包含了一系列定義好的函數
  • #include 語句用於宣告需要使用工具包中的函數
  • 工具包中的函數由其它開發者通過C語言編寫
  • 也可根據專案需要自行編寫私有工具包

小結

  • main() 是C程式中的入口函數(第一個被呼叫的函數)
  • main() 函數被作業系統呼叫(返回值也傳給作業系統)
  • 工具包的本質是一個函數集合
  • 可以根據需要自行定義工具包(函數集)

三、函數定義細節剖析

函數定義與函數呼叫

函數在被呼叫前必須完整定義

函數可以先被宣告,再被定義

  • 宣告時,必須給出函數三要素(函數名,參數列,返回型別)
  • 定義時,必須完整給出函數體定義

特殊的基礎型別

  • C語言中存在空型別(void),這種型別表示“空”
  • void 不能用於定義具體變數(沒有任何資料屬於空型別)
  • void 常用於函數定義,表示無返回值或無引數

void 深入理解

  • void 是基礎型別,但不是基礎資料型別,無法定義變數
  • void可用於函數引數,表示函數無引數
  • void 可用於函數返回型別,表示函數無返回值

所以說,下面程式的寫法就是錯誤的。

#include <stdio.h>
void demo(void i)
{
    return i;
}
int main()
{
    void v;
    void x = v;
    demo(x);
    return 0;
}

注意事項

C語言中的函數如果確定不需要引數,那麼用 void 定義引數,而不是不寫引數。

#include <stdio.h>
void f( )
{
    printf("void f() n");
}
void g(void)
{
    printf("void g() n");
}
int main()
{
    f();
    f(1, 2);
    g();
//    g(1);   // ERROR
    return 0;
}

下面為輸出結果:

可以看出,f 函數的輸入引數是沒有限制的,而g 函數沒有輸入引數,這點要特別注意。

關於函數返回

return 語句直接返回主調函數,後續程式碼不再執行

對於無返回值函數

  • return 可以直接使用,無需跟上返回值
  • 當函數體中沒有 return 語句時,最後一條語句執行後自動返回

對於有返回值的函數

  • return 必須跟上一個合法返回值,所有執行分支都必須顯示返回值
  • return 語句必須出現在函數體中,並且必須被執行

小結

  • 函數可以先被宣告,再被定義(呼叫前必須給出完整定義)
  • C語言中存在空型別(void) ,這種型別表示“空”
  • void是基礎型別,但不是基礎資料型別
  • return語句直接返回主調函數,後續程式碼不再執行
  • 函數中的所有執行分支必須存在return語句

四、函數引數深度剖析

深入函數引數

  • 函數引數在函數定義時並沒有確定的值(形參)
  • 函數引數的具體值在函數呼叫時指定(實參)
  • 函數引數的本質是變數
  • 函數呼叫時指定的實參用於對形參進行初始化,初始化之後形參在函數內部等同於普通變數。

下面看一段程式碼:

#include <stdio.h>
int test(int n);
int main()
{
    int i = 3;
    int j = test(i);
    printf("i = %d, j = %dn", i, j);
    return 0;
}
int test(int n)
{
    n = n * 10;
    return n;
}

下面為輸出結果:

特殊的陣列引數

  • 可以在定義函數時使用陣列形參(如: int f( int a[5] );)
  • 陣列形參需要使用同型別陣列作為實參
  • 在C語言中,陣列作為函數引數傳遞時大小資訊丟失
  • 在函數內部修改陣列形參,將影響陣列實參

注意事項

  • 陣列形參已經發生退化,不包含陣列大小資訊
  • 範例:void func (int a[ ]) 等價於void func (int a[1]) 等價於void func (int a[10]) 等價於void func (int a[100])

下面看一段程式碼,加深印象:

#include <stdio.h>
void demo(int a[3])
{
    a[0] = 50;
}
int sum(int a[], int len)
{
    int ret = 0;
    int i = 0;
    while( i < len )
    {
        ret += a[i];
        i++;
    }
    return ret;
}
int main()
{
    int arr1[5] = {0, 1, 2, 3, 4};      // arr1[0] -> 0
    int arr2[10] = {0, 10, 20, 30, 40}; // arr2[0] -> 0
    demo(arr1);
    demo(arr2);
    printf("arr1[0] = %dn", arr1[0]);
    printf("arr2[0] = %dn", arr2[0]);
    printf("sum(arr1) = %dn", sum(arr1, 5));
    printf("sum(arr2) = %dn", sum(arr2, 10));
    return 0;
}

下面為輸出結果:

這裡注意一下這句話:在函數內部修改陣列形參,將影響陣列實參,所以當呼叫 demo 函數後,實參的 arr1[0] 和 arr2[0] 都變成了50。

小結

  • 函數定義時引數沒有具體值,函數呼叫時指定引數初始值
  • 函數引數在函數內部等同於普通變數
  • 在C語言中,陣列作為函數引數傳遞時大小資訊丟失
  • 在函數內部修改陣列形參,將影響陣列實參
  • 在C語言中,陣列作為函數引數傳遞時大小資訊丟失在函數內部修改陣列形參,將影響陣列實參

五、編寫函數對陣列排序

排序的一般定義

  • 排序是計算機內經常進行的一種操作,其目的是將一組“無序”的資料元素調整為“有序”的資料元素。

排序中的關鍵操作

比較

  • 任意兩個資料元素通過比較操作確定先後次序

交換

  • 資料元素之間需要交換才能得到預期結果

核心思想

  • 每次(例如第i次,i = O,1,..., n-2) 從後面 n-i 個待排的資料元素中選出最小元素,作為第 i 個元素。

解決方案

編寫函數 int Min(int a[], int b, int e) 選擇最小元素

  • 功能定義:在陣列 a 的 b ...e ] 範圍尋找最小元素
  • 返回值:最小元素在陣列中的下標

迴圈遍歷陣列,將每次找到的最小元素交換就位

下面看一下範例程式碼:

#include <stdio.h>
int Min(int a[], int b, int e)
{
    int r = b;
    int i = 0;
    for(i=b; i<=e; i++)
        if( a[r] > a[i] )
            r = i;
    return r;
}
void Sort(int a[], int n)
{
    int i = 0;
    int j = 0;
    int k = 0;
    for(i=0; i<n; i++)
    {
        j = Min(a, i, n-1);
        if( i != j )
        {
            k = a[i];
            a[i] = a[j];
            a[j] = k;
        }
    }
}
void Print(int a[], int n)
{
    int i = 0;
    while( i < n )
        printf("%d ", a[i++]);
    printf("n");
}
int main()
{
    int a[5] = {20, 30, 10, 40, 50};
    printf("Origin: n");
    Print(a, 5);
    Sort(a, 5);
    printf("After: n");
    Print(a, 5);
    return 0;
}

下面為輸出結果:

小結

  • 排序是將一組“無序”的資料調整為“有序”的資料
  • 排序中的關鍵操作為比較和交換
  • 排序時每次選擇未排序資料中的最小值,並交換就位
  • 每次選擇交換使得資料逐漸有序,最終完全有序

六、變數的作用域與生命期(上)

C語言中變數的分類

區域性變數

  • 函數內部定義的變數(隸屬於當前函數)
  • 只能在當前函數中存取使用

全域性變數

  • 全域性範圍內的變數(不特定隸屬於任意一個函數)
  • 可以在任意函數中存取使用

同名變數的問題

  • 不同函數中的區域性變數可以同名(不會產生衝突)
  • 全域性變數不能同名(會產生命名衝突)
  • 當區域性變數和全域性變數同名時,優先使用區域性變數

同名變數規則

  • 存在多個同名變數時,優先使用最近定義的變數。

下面看一段程式碼,感受一下:

#include <stdio.h>
int var = 100;  // 全域性變數
void f(int var) // var <==> 區域性變數
{
    var++;
    printf("var = %dn", var);
}
int main()
{
    int var = 10;  // 區域性變數
    f(var);  // f(10);
    printf("var = %dn", var);  // var = 10;
    return 0;
}

下面為輸出結果:

變數的作用域

  • 變數的作用域指的是變數定義後的可存取範圍
  • 不同變數的作用域可以有重疊
  1. 不同名變數在重疊作用域內可分別存取
  2. 在重疊作用域內,只可存取最近定義的同名變數

區域性變數的作用域

  • 程式碼塊:從 { 開始到 } 結束的一段程式碼
  • 變數只能定義在程式碼塊的開始處,即: { 之後,執行語句之前
  • 變數的作用域從定義開始到當前程式碼塊結束
  • 當變數的作用域結束後,變數不可用 (無法直接存取)

全域性變數的作用域

  • 全域性作用域:可在程式的各個角落存取並使用
  • 檔案作用域:只能在當前程式碼檔案中存取並使用
  • 全域性作用域:可在程式的各個角落存取並使用一檔案作用域:只能在當前程式碼檔案中存取並使用
  • 工程開發中,全域性變數通常以 g_ 作為字首命名(工程約定)

下面看一段程式碼,感受一下:

#include <stdio.h>
int var = 100;  // 全域性變數
int main()
{
    int var = 10;  // 區域性變數
    {
        int var = 1;  // 區域性變數
        printf("var = %dn", var);
    }
    printf("var = %dn", var);  // var = 10;
    return 0;
}

下面為輸出結果:

注意:存在多個同名變數時,優先使用最近定義的變數

小結

  • 區域性變數只能在當前函數中使用,全域性變數可在任何地方使用
  • 當區域性變數和全域性變數同名時,優先使用區域性變數
  • 變數的作用域可重疊,內層作用域覆蓋外層作用域
  • 離開作用域後變數不可存取(無法繼續使用)

七、變數的作用域與生命期(下)

不同變數的物理儲存區域

  • 在現代計算機系統中,實體記憶體被分為不同區域
  • 區域不同,用途不同,不同種類的變數位於不同區域
  1. 全域性資料區:存放全域性變數,靜態變數
  2. 棧空間:存放函數引數,區域性變數
  3. 堆空間:用於動態建立變數

生命期:變數從建立到銷燬的時間(即:合法可用的時間)

不同變數的生命期

全域性資料區中的變數

  • 程式開始執行時建立,程式結束時被銷燬,整個程式執行期合法可用

棧空間中的變數

  • 進入作用域時建立,離開作用域時銷燬(自動銷燬)

區域性變數在函數呼叫返回後銷燬

下面看一段程式碼,感受一下變數生命期:

#include <stdio.h>
int var = 1;
void func()
{
    printf("var = %dn", var);
}
int main()
{
    int var = 2;
    int i = 0;
    for(i=0; i<5; i++)
    {
        int var = 4;
        var += i;
        printf("var = %dn", var);
    }
    func();
    printf("var = %dn", var);
    return 0;
}

下面為輸出結果:

這個例子充分展示了變數的生命週期,值得仔細體會。

作用域與生命期無本質聯絡

作用域規則是語法層面對變數是否可存取的規定

生命期是二進位制層面上變數存在於記憶體中的時間

可能的情況

  • 作用域外無法存取的變數,可能在其生命期中(靜態區域性變數)
  • 作用域內可存取的變數,可能已經被銷燬(堆變數)
  • 生命期中的變數,可能無法存取(檔案作用域全域性變數)

靜態變數

  • static 是C語言中的關鍵字
  • static 修飾的區域性變數建立於全域性資料區(擁有程式生命期)
  • static 修飾的全域性變數只有檔案作用域(檔案之外無法存取)
  • static 區域性變數只會初始化一次,作用域與普通變數無異

​​​​​​​變數的生命期由變數儲存位置決定 ​​​​​​​

  • static 將變數儲存於全域性資料區,預設初始化為0
  • auto 將變數儲存於棧空間,預設初始化為隨機值
  • register 將變數儲存於暫存器,預設初始化為隨機值​​​​​​​​​​​​​​

不同型別變數範例​​​​​​​​​​​​​​

#include <stdio.h>
int g_var = 1;
static int g_sVar = 2;
int main()
{
    static int s_var = 3;
    auto int v = 4;
    register int rv = 5;
    printf("g_var = %dn", g_var);
    printf("g_sVar = %dn", g_sVar);
    printf("s_var = %dn", s_var);
    printf("v     = %dn", v);
    printf("rv    = %dn", rv);
    return 0;
}

下面為輸出結果:

下面看一段程式碼,感受一下 static 關鍵詞:

#include <stdio.h>
int global;
int func(int x)
{
    static int s_var;   // 全域性資料區中的變數,預設初始化為 0
                        // 並且,只做一次初始化
    s_var += x;
    return s_var;
}
int main()
{
    int i = 0;
    for(i=1; i<=5; i++)
    {
        printf("func(%d) = %dn", i, func(i));
    }
    printf("func(0) = %dn", func(0));
    printf("global = %dn", global);
    return 0;
}

下面為輸出結果:

這裡注意:全域性資料區中的變數,預設初始化為 0 ,並且,只做一次初始化

小結

  • 變數生命期指變數合法可用的時間
  • 生命期是變數存在於記憶體中的時間
  • 作用域與生命期無本質聯絡
  • 作用域和生命期用於判斷變數是否可存取
 staticauto(預設)register
區域性變數全域性資料區棧空間暫存器(可能)
全域性變數全域性資料區------

八、函數專題練習

題目:編寫函數,將字串轉換為整型數

函數原型:int str2int(char s[]);

引數:可以代表整型數的字串

返回值:整型值

注意事項:

  • 整型數可以存在符號位,如: "-12345"
  • 字串本身可能不是一個合法整型數,如:"123xyz45"

演演算法流程

上程式碼:

#include <stdio.h>
int getNumber(char c)
{
    int ret = -1;
    if( ('0' <= c) && (c <= '9') )
        ret = c - '0';
    return ret;
}
int str2int(char str[])
{
    int ret = 0;
    int sign = 0;
    int i = 0;
    if( getNumber(str[0]) != -1 )
    {
        sign = 1;
        i = 0;
    }
    else if( str[0] == '+' )
    {
        sign = 1;
        i = 1;
    }
    else if( str[0] == '-' )
    {
        sign = -1;
        i = 1;
    }
    while( sign && str[i] )
    {
        int n = getNumber(str[i]);
        if( n != -1 )
            ret = ret * 10 + n;
        else
            break;
        i++;
    }
    ret = sign * ret;
    return ret;
}
int main()
{
    printf("%dn", str2int("123"));
    printf("%dn", str2int("-12345"));
    printf("%dn", str2int("567xyz89"));
    printf("%dn", str2int("abc"));
    printf("%dn", str2int("-xyz"));
    return 0;
}

下面為輸出結果:

九、遞迴函數簡介

在程式設計中,將函數自呼叫稱為遞迴呼叫

遞迴是一種數學上分而自治的思想 ​​​​​​​

  • 將原問題分解為規模較小的問題進行處理
  • 問題的分解是有限的(遞迴不能無限進行)

遞迴模型的一般表示法

遞迴在程式設計中的應用

遞迴函數

  • 函數體中存在自我呼叫的函數
  • 遞迴函數必須有遞迴出口(邊界條件)
  • 函數的無限遞迴將導致程式崩潰​​​​​​​​​​​​​​

遞迴思想的應用:​​​​​​​​​​​​​

自然數列求和:sum( n ) = 1 +2 +3 + ... + n​​​​​​

斐波拉契數列:1,1,2,3,5,8,13,21,...

上程式碼:

#include <stdio.h>
int sum(int n)
{
    int ret = 0;
    if( n == 1 )
        ret = 1;
    else
        ret = n + sum(n-1);
    return ret;
}
int fac(int n)
{
    int ret = 0;
    if( n == 1 )
        ret = 1;
    else if( n == 2 )
        ret = 1;
    else if( n >= 3 )
        ret = fac(n-1) + fac(n-2);
    else
        ret = -1;
    return ret;
}
int main()
{
    int i = 0;
    printf("sum(1) = %dn", sum(1));
    printf("sum(10) = %dn", sum(10));
    printf("sum(100) = %dn", sum(100));
    for(i=1; i<=10; i++)
    {
        printf("%d, ", fac(i));
    }
    printf("n");
    return 0;
}

下面為輸出結果:

小結

  • 遞迴是一種數學上分而自治的思想
  • 遞迴程式中的表現為函數自呼叫
  • 遞迴解法必須要有邊界條件,否則無解
  • 編寫遞迴函數時不要陷入函數的執行細節

到此這篇關於C語言從零探索函數的知識的文章就介紹到這了,更多相關C語言函數內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com