天天看點

C語言進階(九)—— 函數指針和回調函數、預處理、動态庫和靜态庫的使用、遞歸函數1. 函數指針2. 預處理3. 動态庫的封裝和使用4.遞歸函數

1. 函數指針

1.1 函數類型

通過什麼來區分兩個不同的函數?

一個函數在編譯時被配置設定一個入口位址,這個位址就稱為函數的指針,函數名代表函數的入口位址。

函數三要素: 名稱、參數、傳回值。C語言中的函數有自己特定的類型。

c語言中通過typedef為函數類型重命名:

typedef int f(int, int);        // f 為函數類型
typedef void p(int);        // p 為函數類型
           

這一點和數組一樣,是以我們可以用一個指針變量來存放這個入口位址,然後通過該指針變量調用函數。

注意:通過函數類型定義的變量是不能夠直接執行,因為沒有函數體。隻能通過類型定義一個函數指針指向某一個具體函數,才能調用。

typedef int(p)(int, int);

void my_func(int a,int b){
    printf("%d %d\n",a,b);
}

void test(){

    p p1;
    //p1(10,20); //錯誤,不能直接調用,隻描述了函數類型,但是并沒有定義函數體,沒有函數體無法調用
    p* p2 = my_func;
    p2(10,20); //正确,指向有函數體的函數入口位址
}
           

1.2 函數指針(指向函數的指針)

函數指針定義方式:

  • 先定義函數類型,根據類型定義指針變量;
  • 先定義函數指針類型,根據類型定義指針變量;
  • 直接定義函數指針變量;
int my_func(int a,int b){
    printf("ret:%d\n", a + b);
    return 0;
}

//1. 先定義函數類型,通過類型定義指針
void test01(){
    typedef int(FUNC_TYPE)(int, int);
    FUNC_TYPE* f = my_func;
    //如何調用?
    (*f)(10, 20);
    f(10, 20);
}

//2. 定義函數指針類型
void test02(){
    typedef int(*FUNC_POINTER)(int, int);
    FUNC_POINTER f = my_func;
    //如何調用?
    (*f)(10, 20);
    f(10, 20);
}

//3. 直接定義函數指針變量
void test03(){
    
    int(*f)(int, int) = my_func;
    //如何調用?
    (*f)(10, 20);
    f(10, 20);
}
           

1.3 函數指針數組

函數指針數組,每個元素都是函數指針。

void func01(int a){
    printf("func01:%d\n",a);
}
void func02(int a){
    printf("func02:%d\n", a);
}
void func03(int a){
    printf("func03:%d\n", a);
}

void test(){

#if 0
    //定義函數指針
    void(*func_array[])(int) = { func01, func02, func03 };
#else
    void(*func_array[3])(int);
    func_array[0] = func01;
    func_array[1] = func02;
    func_array[2] = func03;
#endif

    for (int i = 0; i < 3; i ++){
        func_array[i](10 + i);
        (*func_array[i])(10 + i);
    }
}
           

1.4 函數指針做函數參數(回調函數)

函數參數除了是普通變量,還可以是函數指針變量。

//形參為普通變量
void fun( int x ){}
//形參為函數指針變量
void fun( int(*p)(int a) ){}
           

函數指針變量常見的用途之一是把指針作為參數傳遞到其他函數,指向函數的指針也可以作為參數,以實作函數位址的傳遞。

int plus(int a, int b){
    return a + b;
}

int sub(int a, int b){
    return a - b;
}

int mul(int a, int b){
    return a * b;
}

int division(int a, int b){
    return a / b;
}

//函數指針 做函數的參數 --- 回調函數
void Calculator(int(*myCalculate)(int, int), int a, int b)
{
    int ret = myCalculate(a, b); //dowork中不确定使用者選擇的内容,由後期來指定運算規則
    printf("ret = %d\n", ret);
}

void test01()
{
    printf("請輸入操作符\n");
    printf("1、+ \n");
    printf("2、- \n");
    printf("3、* \n");
    printf("4、/ \n");

    int select = -1;
    scanf("%d", &select);

    int num1 = 0;
    printf("請輸入第一個操作數:\n");
    scanf("%d", &num1);

    int num2 = 0;
    printf("請輸入第二個操作數:\n");
    scanf("%d", &num2);

    switch (select)
    {
    case  1:
        Calculator(plus, num1, num2);
        break;
    case  2:
        Calculator(sub, num1, num2);
        break;
    case 3:
        Calculator(mul, num1, num2);
        break;
    case 4:
        Calculator(division, num1, num2);
        break;
    default:
        break;
    }

}
           

注意:函數指針和指針函數的差別:

  • 函數指針是指向函數的指針;
  • 指針函數是傳回類型為指針的函數;

2. 預處理

2.1 預處理的基本概念

C語言對源程式處理的四個步驟:預處理、編譯、彙編、連結。

預處理是在程式源代碼被編譯之前,由預處理器(Preprocessor)對程式源代碼進行的處理。這個過程并不對程式的源代碼文法進行解析,但它會把源代碼分割或處理成為特定的符号為下一步的編譯做準備工作。

2.2 檔案包含指令(#include)

2.2.1 檔案包含處理

檔案包含處理”是指一個源檔案可以将另外一個檔案的全部内容包含進來。C語言提供了#include指令用來實作“檔案包含”的操作。

C語言進階(九)—— 函數指針和回調函數、預處理、動态庫和靜态庫的使用、遞歸函數1. 函數指針2. 預處理3. 動态庫的封裝和使用4.遞歸函數

2.2.2 #incude<>和#include""差別

  • "" 表示系統先在file1.c所在的目前目錄找file1.h,如果找不到,再按系統指定的目錄檢索。
  • < > 表示系統直接按系統指定的目錄檢索。

注意:

1.#include <>常用于包含庫函數的頭檔案;

2.#include ""常用于包含自定義的頭檔案;

3. 理論上#include可以包含任意格式的檔案(.c .h等) ,但一般用于頭檔案的包含;

2.3 宏定義

2.3.1 無參數的宏定義(宏常量)

如果在程式中大量使用到了100這個值,那麼為了友善管理,我們可以将其定義為:

const int num =100; 但是如果我們使用num定義一個數組,在不支援c99标準的編譯器上是不支援的,因為num不是一個編譯器常量,如果想得到了一個編譯器常量,那麼可以使用:

#define num 100

在編譯預處理時,将程式中在該語句以後出現的所有的num都用100代替。這種方法使使用者能以一個簡單的名字代替一個長的字元串,在預編譯時将宏名替換成字元串的過程稱為“宏展開”。宏定義,隻在宏定義的檔案中起作用。

#define PI 3.1415
void test(){
    double r = 10.0;
    double s = PI * r * r;
    printf("s = %lf\n", s);
}
           

說明:

  1. 宏名一般用大寫,以便于與變量差別;
  1. 宏定義可以是常數、表達式等;
  1. 宏定義不作文法檢查,隻有在編譯被宏展開後的源程式才會報錯;
  1. 宏定義不是C語言,不在行末加分号;
  1. 宏名有效範圍為從定義到本源檔案結束;
  1. 可以用#undef指令終止宏定義的作用域;
  1. 在宏定義中,可以引用已定義的宏名;

2.3.2 帶參數的宏定義(宏函數)

在項目中,經常把一些短小而又頻繁使用的函數寫成宏函數,這是由于宏函數沒有普通函數參數壓棧、跳轉、傳回等的開銷,可以調高程式的效率。

宏通過使用參數,可以建立外形和作用都與函數類似地類函數宏(function-likemacro). 宏的參數也用圓括号括起來。

#define SUM(x,y) (( x )+( y ))
void test(){
    
    //僅僅隻是做文本替換 下例替換為 int ret = ((10)+(20));
    //不進行計算
    int ret = SUM(10, 20);
    printf("ret:%d\n",ret);
}
           

注意:

  1. 宏的名字中不能有空格,但是在替換的字元串中可以有空格。ANSI C允許在參數清單中使用空格;
  1. 用括号包覆每一個參數,并包覆宏的整體定義。
  1. 用大寫字母表示宏的函數名。
  1. 如果打算宏代替函數來加快程式運作速度。假如在程式中隻使用一次宏對程式的運作時間沒有太大提高。

2.4 條件編譯

2.4.1 基本概念

一般情況下,源程式中所有的行都參加編譯。但有時希望對部分源程式行隻在滿足一定條件時才編譯,即對這部分源程式行指定編譯條件。

C語言進階(九)—— 函數指針和回調函數、預處理、動态庫和靜态庫的使用、遞歸函數1. 函數指針2. 預處理3. 動态庫的封裝和使用4.遞歸函數

2.4.2 條件編譯

  • 防止頭檔案被重複包含引用;
#ifndef _SOMEFILE_H
#define _SOMEFILE_H

//需要聲明的變量、函數
//宏定義
//結構體

#endif
           

2.5 一些特殊的預定宏

C編譯器,提供了幾個特殊形式的預定義宏,在實際程式設計中可以直接使用,很友善。

//    __FILE__            宏所在檔案的源檔案名 
//    __LINE__            宏所在行的行号
//    __DATE__            代碼編譯的日期
//    __TIME__            代碼編譯的時間

void test()
{
    printf("%s\n", __FILE__);
    printf("%d\n", __LINE__);
    printf("%s\n", __DATE__);
    printf("%s\n", __TIME__);
}
           

3. 動态庫的封裝和使用

3.1 庫的基本概念

庫是已經寫好的、成熟的、可複用的代碼。每個程式都需要依賴很多底層庫,不可能每個人的代碼從零開始編寫代碼,是以庫的存在具有非常重要的意義。

在我們的開發的應用中經常有一些公共代碼是需要反複使用的,就把這些代碼編譯為庫檔案。

庫可以簡單看成一組目标檔案的集合,将這些目标檔案經過壓縮打包之後形成的一個檔案。像在Windows這樣的平台上,最常用的c語言庫是由內建按開發環境所附帶的運作庫,這些庫一般由編譯廠商提供。

3.2 windows下靜态庫建立和使用

3.2.1 靜态庫的建立

  1. 建立一個新項目,在已安裝的模闆中選擇“正常”,在右邊的類型下選擇“空項目”,在名稱和解決方案名稱中輸入staticlib。點選确定。
  1. 在解決方案資料總管的頭檔案中添加,mylib.h檔案,在源檔案添加mylib.c檔案(即實作檔案)。
  1. 在mylib.h檔案中添加如下代碼:
#ifndef TEST_H
#define TEST_H

int myadd(int a,int b);
#endif
           
  1. 在mylib.c檔案中添加如下代碼:
#include"test.h"
int myadd(int a, int b){
    return a + b;
}
           
  1. 配置項目屬性。因為這是一個靜态連結庫,是以應在項目屬性的“配置屬性”下選擇“正常”,在其下的配置類型中選擇“靜态庫(.lib)。
  1. 編譯生成新的解決方案,在Debug檔案夾下會得到mylib.lib (對象檔案庫),将該.lib檔案和相應頭檔案給使用者,使用者就可以使用該庫裡的函數了。

3.2.2 靜态庫的使用

方法一:配置項目屬性

  1. 添加工程的頭檔案目錄:工程---屬性---配置屬性---c/c++---正常---附加包含目錄:加上頭檔案存放目錄。
  1. 添加檔案引用的lib靜态庫路徑:工程---屬性---配置屬性---連結器---正常---附加庫目錄:加上lib檔案存放目錄。
  1. 然後添加工程引用的lib檔案名:工程---屬性---配置屬性---連結器---輸入---附加依賴項:加上lib檔案名。

方法二:使用編譯語句

#pragma comment(lib,"./mylib.lib")
           

方法三:添加工程中

  1. 就像你添加.h和.c檔案一樣,把lib檔案添加到工程檔案清單中去.
  1. 切換到"解決方案視圖",--->選中要添加lib的工程-->點選右鍵-->"添加"-->"現有項"-->選擇lib檔案-->确定.

3.2.3 靜态庫優缺點

  • 靜态庫對函數庫的連結是放在編譯時期完成的,靜态庫在程式的連結階段被複制到了程式中,和程式運作的時候沒有關系;
  • 程式在運作時與函數庫再無瓜葛,移植友善。
  • 浪費空間和資源,所有相關的目标檔案與牽涉到的函數庫被連結合成一個可執行檔案。

記憶體和磁盤空間

靜态連結這種方法很簡單,原理上也很容易了解,在作業系統和硬體不發達的早期,絕大部門系統采用這種方案。随着計算機軟體的發展,這種方法的缺點很快暴露出來,那就是靜态連結的方式對于計算機記憶體和磁盤空間浪費非常嚴重。特别是多程序作業系統下,靜态連結極大的浪費了記憶體空間。在現在的linux系統中,一個普通程式會用到c語言靜态庫至少在1MB以上,那麼如果磁盤中有2000個這樣的程式,就要浪費将近2GB的磁盤空間。

程式開發和釋出

空間浪費是靜态連結的一個問題,另一個問題是靜态連結對程式的更新、部署和釋出也會帶來很多麻煩。比如程式中所使用的mylib.lib是由一個第三方廠商提供的,當該廠商更新容量mylib.lib的時候,那麼我們的程式就要拿到最新版的mylib.lib,然後将其重新編譯連結後,将新的程式整個釋出給使用者。這樣的做缺點很明顯,即一旦程式中有任何子產品更新,整個程式就要重新編譯連結、釋出給使用者,使用者要重新安裝整個程式。

3.3 windows下動态庫建立和使用

要解決空間浪費和更新困難這兩個問題,最簡單的辦法就是把程式的子產品互相分割開來,形成獨立的檔案,而不是将他們靜态的連結在一起。簡單地講,就是不對哪些組成程式的目标程式進行連結,等程式運作的時候才進行連結。也就是說,把整個連結過程推遲到了運作時再進行,這就是動态連結的基本思想。

3.3.1 動态庫的建立

  1. 建立一個新項目,在已安裝的模闆中選擇“正常”,在右邊的類型下選擇“空項目”,在名稱和解決方案名稱中輸入mydll。點選确定。
  1. 在解決方案資料總管的頭檔案中添加,mydll.h檔案,在源檔案添加mydll.c檔案(即實作檔案)。
  1. 在mydll.h檔案中添加如下代碼:
#ifndef TEST_H
#define TEST_H

__declspec(dllexport) int myminus(int a, int b);

#endif
           
  1. 在mydll.c檔案中添加如下代碼:
#include"test.h"
__declspec(dllexport) int myminus(int a, int b){
    return a - b;
}
           
  1. 配置項目屬性。因為這是一個動态連結庫,是以應在項目屬性的“配置屬性”下選擇“正常”,在其下的配置類型中選擇“動态庫(.dll)。
  1. 編譯生成新的解決方案,在Debug檔案夾下會得到mydll.dll (對象檔案庫),将該.dll檔案、.lib檔案和相應頭檔案給使用者,使用者就可以使用該庫裡的函數了。

疑問一:__declspec(dllexport)是什麼意思?

動态連結庫中定義有兩種函數:導出函數(export function)和内部函數(internal function)。 導出函數可以被其它子產品調用,内部函數在定義它們的DLL程式内部使用。

疑問二:動态庫的lib檔案和靜态庫的lib檔案的差別?

使用動态庫的時候,往往提供兩個檔案:一個引入庫(.lib)檔案(也稱“導入庫檔案”)和一個DLL(.dll)檔案。雖然引入庫的字尾名也是“lib”,但是,動态庫的引入庫檔案和靜态庫檔案有着本質的差別, 對一個DLL檔案來說,其引入庫檔案(.lib)包含該DLL導出的函數和變量的符号名,而.dll檔案包含該DLL實際的函數和資料。在使用動态庫的情況下,在編譯連結可執行檔案時,隻需要連結該DLL的引入庫檔案,該DLL中的函數代碼和資料并不複制到可執行檔案,直到可執行程式運作時,才去加載所需的DLL,将該DLL映射到程序的位址空間中,然後通路DLL中導出的函數。

3.3.2 動态庫的使用

方法一:隐式調用

建立主程式TestDll,将mydll.h、mydll.dll和mydll.lib複制到源代碼目錄下。

(P.S:頭檔案Func.h并不是必需的,隻是C++中使用外部函數時,需要先進行聲明)

在程式中指定連結引用連結庫 : #pragmacomment(lib,"./mydll.lib")

方法二:顯式調用

HANDLE hDll; //聲明一個dll執行個體檔案句柄
hDll = LoadLibrary("mydll.dll"); //導入動态連結庫
MYFUNC minus_test; //建立函數指針
//擷取導入函數的函數指針
minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");
           

4.遞歸函數

4.1 遞歸函數基本概念

C通過運作時堆棧來支援遞歸函數的實作。遞歸函數就是直接或間接調用自身的函數。

注意:遞歸函數必須要有結束條件,否則進入死循環

4.2 普通函數調用

void funB(int b){
    printf("b = %d\n", b);
}

void funA(int a){
    funB(a - 1);
    printf("a = %d\n", a);
}

int main(void){
    funA(2);
    printf("main\n");
    return 0;
}
           

函數的調用流程如下:

C語言進階(九)—— 函數指針和回調函數、預處理、動态庫和靜态庫的使用、遞歸函數1. 函數指針2. 預處理3. 動态庫的封裝和使用4.遞歸函數

4.3 遞歸函數調用

遞歸實作給出一個數8793,依次列印千位數字8、百位數字7、十位數字9、個位數字3。

void recursion(int val){
    if (val == 0){
        return;
    }
    int ret = val / 10;
    recursion(ret);
    printf("%d ",val % 10);
}
           

4.4 遞歸實作字元串反轉

int reverse1(char *str){
    if (str == NULL)
    {
        return -1;
    }

    if (*str == '\0') // 函數遞歸調用結束條件
    {
        return 0;
    }
    
    reverse1(str + 1);
    printf("%c", *str);

    return 0;
}
           

4.5 斐波那契數列 案例

int fibonacci(int pos){
    if(pos == 1 || pos == 2) {
        return 1;
    }
    return fibonacci(pos -2) + fibonacci(pos -1);
}
           

繼續閱讀