第 16 節

函數指針

0瀏覽次數0訪問次數--跳出率--平均停留

函數指針

閱讀提示: 本章前半部分只使用已經學過的函數、指針和類,需要按順序掌握。後半部分會擴展到 nullptrusing、Lambda 和 std::function 等現代 C++ 特性。擴展部分會提前解釋新語法和 API 的基本用法,但需要學完後面的“現代 C++”章節才能完全看懂。初次學習時可以先了解結論,暫時跳過擴展部分。

函數指針的基本概念

前面已經學習過:普通指針可以保存變量的地址,通過指針能夠間接訪問變量。

函數編譯後也存放在內存中,因此函數同樣有地址。函數指針就是保存函數地址的指針變量。通過函數指針,可以間接調用它所指向的函數。

函數指針常用於:

  • 把一個函數傳給另一個函數,也就是回調函數
  • 在程序運行過程中選擇不同的處理函數
  • 把多個形式相同的函數放進表中統一管理
  • 保存並調用類的成員函數

假設有下面的函數:

int add(int a, int b);

指向它的函數指針可以聲明爲:

int (*function_pointer)(int, int);

這條聲明可以從變量名向外閱讀:

  1. function_pointer 是變量名。
  2. *function_pointer 說明它是一個指針。
  3. (int, int) 說明它指向的函數接收兩個 int 參數。
  4. 最前面的 int 說明該函數返回一個 int

函數指針的基本語法爲:

返回值类型 (*函数指针变量名)(参数类型列表);

變量名兩側的括號不能省略:

int (*operation)(int, int); // operation 是函数指针
int *operation(int, int);   // operation 是函数,返回值是 int*

總結:函數指針類型不僅包含返回值類型,還包含完整的參數類型列表。

示例1:保存函數地址並調用函數

函數名在大多數表達式中會自動轉換爲函數地址,因此可以直接把 add 賦給函數指針。也可以寫成 &add,其中 & 表示獲取函數地址,兩種寫法效果相同。

通過函數指針調用函數也有兩種寫法:

function_pointer(参数);    // 推荐写法
(*function_pointer)(参数); // 先解引用再调用

示例:

#include <iostream>

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

int main()
{
    int (*function_pointer)(int, int) = add;

    std::cout << function_pointer(3, 5) << "\n";
    std::cout << (*function_pointer)(10, 20) << "\n";

    function_pointer = &add;
    std::cout << function_pointer(7, 8) << "\n";

    return 0;
}

運行結果:

8
30
15

總結:通常直接用函數名賦值,並使用 function_pointer(...) 調用,代碼更簡潔。

示例2:函數指針類型必須匹配

函數指針只能指向類型兼容的函數。返回值類型和每個參數的類型都要匹配。

示例:

#include <iostream>

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

double divide(double a, double b)
{
    return a / b;
}

int main()
{
    int (*integer_operation)(int, int) = add;
    double (*decimal_operation)(double, double) = divide;

    std::cout << integer_operation(4, 6) << "\n";
    std::cout << decimal_operation(7.0, 2.0) << "\n";

    // integer_operation = divide;
    // 错误:divide 的类型是 double(double, double),类型不匹配

    return 0;
}

運行結果:

10
3.5

注意:不要依賴參數調用時可能發生的隱式類型轉換。函數本身的類型必須與函數指針類型兼容。

示例3:運行時切換不同的函數

只要函數類型相同,一個函數指針就可以在不同時間指向不同函數。這可以把“選擇執行哪個操作”和“執行操作”分開。

示例:

#include <iostream>

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

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

int main()
{
    int (*operation)(int, int) = add;
    std::cout << "add: " << operation(10, 4) << "\n";

    operation = subtract;
    std::cout << "subtract: " << operation(10, 4) << "\n";

    return 0;
}

運行結果:

add: 14
subtract: 6

示例4:把函數指針作爲參數——回調函數

函數指針也可以作爲另一個函數的參數。被傳入並由其他代碼調用的函數通常稱爲回調函數

回調函數不是一種新的函數類型。“回調”描述的是函數的使用方式:調用者把函數地址交給另一段代碼,另一段代碼在需要時調用這個函數。

下面的 calculate 不關心具體執行加法還是乘法,只要求傳入的函數能夠接收兩個 int 並返回 int

示例:

#include <iostream>

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

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

int calculate(int a, int b, int (*operation)(int, int))
{
    return operation(a, b);
}

int main()
{
    std::cout << calculate(6, 4, add) << "\n";
    std::cout << calculate(6, 4, multiply) << "\n";

    return 0;
}

運行結果:

10
24

總結:回調可以讓 calculate 保持不變,只通過傳入不同函數來改變計算行爲。

示例5:函數指針作爲參數時也可以省略參數名

函數聲明只需要告訴編譯器參數類型,不一定要寫參數名。函數指針參數也遵守這個規則。

下面兩條函數聲明含義相同:

int calculate(int a, int b, int (*operation)(int, int));
int calculate(int, int, int (*)(int, int));

第一種寫法更容易閱讀,通常建議在教程、頭文件和函數定義中保留有意義的參數名。

示例:

#include <iostream>

void print_number(int value)
{
    std::cout << value << "\n";
}

void run_twice(int value, void (*callback)(int))
{
    callback(value);
    callback(value);
}

int main()
{
    run_twice(42, print_number);
    return 0;
}

運行結果:

42
42

示例6:函數指針數組

如果多個函數擁有相同類型,就可以把它們的地址保存在同一個數組中,再通過數組下標選擇函數。

函數指針數組的基本語法爲:

返回值类型 (*数组名[元素个数])(参数类型列表);

數組下標必須在有效範圍內。下面的數組有 3 個元素,因此有效下標是 012

示例:

#include <iostream>

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

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

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

int main()
{
    int (*operations[3])(int, int) = {add, subtract, multiply};

    std::cout << operations[0](8, 2) << "\n";
    std::cout << operations[1](8, 2) << "\n";
    std::cout << operations[2](8, 2) << "\n";

    return 0;
}

運行結果:

10
6
16

這種根據下標選擇行爲的寫法稱爲表驅動。它可以減少很長的 if...elseswitch,但使用下標前必須檢查範圍。

示例7:函數重載與函數指針

前面已經學習過函數重載:多個函數可以使用同一個名字,只要參數列表不同。

當重載函數名賦給類型明確的函數指針時,編譯器會根據函數指針的類型選擇匹配的重載版本。

示例:

#include <iostream>

int square(int value)
{
    return value * value;
}

double square(double value)
{
    return value * value;
}

int main()
{
    int (*integer_square)(int) = square;
    double (*decimal_square)(double) = square;

    std::cout << integer_square(5) << "\n";
    std::cout << decimal_square(1.5) << "\n";

    return 0;
}

運行結果:

25
2.25

如果沒有足夠的類型信息,單獨寫一個重載函數名可能產生二義性。現代 C++ 擴展部分會介紹如何使用顯式類型轉換進一步指定重載版本。

成員函數指針

普通函數指針與成員函數指針的區別

非靜態成員函數必須依附某個對象調用,因爲函數內部可能訪問該對象的數據成員和 this 指針。因此,非靜態成員函數不能保存在普通函數指針中,需要使用成員函數指針

成員函數指針的類型中必須寫出它所屬的類:

返回值类型 (类名::*指针变量名)(参数类型列表);

取得成員函數地址時必須寫 &类名::函数名。調用時還需要提供一個對象:

(对象.*成员函数指针)(参数);
(对象指针->*成员函数指针)(参数);

示例8:靜態成員函數使用普通函數指針

靜態成員函數屬於類,但不依附某個具體對象,也沒有 this 指針。因此,可以使用普通函數指針保存靜態成員函數的地址。

示例:

#include <iostream>

class Calculator
{
public:
    static int add(int a, int b)
    {
        return a + b;
    }
};

int main()
{
    int (*operation)(int, int) = &Calculator::add;
    std::cout << operation(6, 9) << "\n";

    return 0;
}

運行結果:

15

示例9:通過對象調用非靜態成員函數指針

下面的 scale 是非靜態成員函數,會訪問當前對象的 factor_,因此調用時必須指定一個 Multiplier 對象。

運算符 .* 表示“通過對象使用成員指針”。調用表達式外層的括號不能省略,因爲函數調用運算符 () 的優先級高於 .*

示例:

#include <iostream>

class Multiplier
{
private:
    int factor_;

public:
    Multiplier(int factor)
        : factor_(factor)
    {
    }

    int scale(int value)
    {
        return value * factor_;
    }
};

int main()
{
    int (Multiplier::*operation)(int) = &Multiplier::scale;

    Multiplier twice(2);
    Multiplier triple(3);

    std::cout << (twice.*operation)(5) << "\n";
    std::cout << (triple.*operation)(5) << "\n";

    return 0;
}

運行結果:

10
15

同一個成員函數指針配合不同對象調用時,會訪問各個對象自己的數據。

示例10:通過對象指針調用成員函數指針

如果手中保存的是對象指針,需要使用 ->* 運算符。

(对象指针->*成员函数指针)(参数);

示例:

#include <iostream>

class Counter
{
private:
    int value_;

public:
    Counter()
        : value_(0)
    {
    }

    void add(int amount)
    {
        value_ += amount;
    }

    int value()
    {
        return value_;
    }
};

int main()
{
    void (Counter::*action)(int) = &Counter::add;

    Counter counter;
    Counter* object_pointer = &counter;

    (object_pointer->*action)(5);
    (object_pointer->*action)(7);

    std::cout << counter.value() << "\n";

    return 0;
}

運行結果:

12

總結:對象使用 .*,對象指針使用 ->*

示例11:const 成員函數指針

成員函數參數列表後的 const 表示該成員函數不會修改對象的普通數據成員,可以在 const 對象上調用。

const 是成員函數類型的一部分。因此,指向 const 成員函數時,函數指針聲明中也必須帶有 const

示例:

#include <iostream>

class Sensor
{
private:
    int value_;

public:
    Sensor(int value)
        : value_(value)
    {
    }

    int read() const
    {
        return value_;
    }
};

int main()
{
    int (Sensor::*reader)() const = &Sensor::read;

    const Sensor sensor(42);
    std::cout << (sensor.*reader)() << "\n";

    return 0;
}

運行結果:

42

下面兩個成員函數指針類型並不相同:

int (Sensor::*reader1)();       // 指向非 const 成员函数
int (Sensor::*reader2)() const; // 指向 const 成员函数

現代 C++ 擴展(可選讀)

以下內容用於說明函數指針與現代 C++ 特性的聯繫。這裏會解釋示例所需的基本語法,但完整理解仍需要學習後面的“現代 C++”章節。初次學習可以先閱讀結論,以後再回來複習。

擴展示例1:使用 nullptr 表示空函數指針

需要的標準版本: C++11。

nullptr 是 C++11 引入的空指針字面量,表示“沒有指向任何對象或函數”。它比舊式的 NULL 和數字 0 類型更明確。

函數指針可以放進 if 條件中檢查:非空指針轉換爲 true,空指針轉換爲 false調用空函數指針會產生未定義行爲,因此可能爲空時必須先檢查。

示例:

#include <iostream>

void greet()
{
    std::cout << "hello\n";
}

int main()
{
    void (*task)() = nullptr;

    if (task)
    {
        task();
    }
    else
    {
        std::cout << "task is empty\n";
    }

    task = greet;

    if (task != nullptr)
    {
        task();
    }

    return 0;
}

運行結果:

task is empty
hello

注意:函數指針只保存函數地址,不負責管理內存,不需要也不能對它使用 deletenullptr 的完整用法見後面的 nullptr 章節。

擴展示例2:使用 using 給函數指針類型起別名

需要的標準版本: C++11。

直接書寫函數指針類型比較困難。C++11 可以使用 using 給已有類型起別名:

using 新名字 = 已有类型;

下面的語句表示 Operationint (*)(int, int) 的別名:

using Operation = int (*)(int, int);

using 不會創建新的類型,也沒有運行時開銷,只是讓類型更容易閱讀。

示例:

#include <iostream>

using Operation = int (*)(int, int);

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

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

int calculate(int a, int b, Operation operation)
{
    return operation(a, b);
}

int main()
{
    Operation operation = add;
    std::cout << calculate(3, 8, operation) << "\n";

    operation = multiply;
    std::cout << calculate(3, 8, operation) << "\n";

    return 0;
}

運行結果:

11
24

舊代碼中也可能看到 typedef 寫法,兩者含義相同:

typedef int (*Operation1)(int, int);
using Operation2 = int (*)(int, int);

現代 C++ 通常優先使用更容易閱讀的 using。完整用法見後面的 using 章節。

擴展示例3:從函數返回函數指針

有了類型別名,函數也可以方便地返回一個函數指針。下面的 select_operation 根據字符返回對應函數的地址。如果字符無法識別,則返回 nullptr

示例:

#include <iostream>

using Operation = int (*)(int, int);

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

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

Operation select_operation(char symbol)
{
    if (symbol == '+')
    {
        return add;
    }

    if (symbol == '-')
    {
        return subtract;
    }

    return nullptr;
}

int main()
{
    Operation operation = select_operation('-');

    if (operation)
    {
        std::cout << operation(20, 7) << "\n";
    }
    else
    {
        std::cout << "unknown operation\n";
    }

    return 0;
}

運行結果:

13

總結:複雜的函數指針類型應先使用 using 起別名,再作爲參數類型或返回值類型。

擴展示例4:無捕獲 Lambda 轉換爲函數指針

需要的標準版本: C++11。

Lambda 表達式可以在使用位置定義一個小型匿名函數。基本語法如下:

[捕获列表](参数列表) -> 返回值类型
{
    函数体
};

返回值類型通常可以由編譯器推導,因此 -> 返回值类型 經常省略。

當捕獲列表爲空,也就是寫成 [] 時,Lambda 可以轉換爲簽名兼容的普通函數指針。

示例:

#include <iostream>

using Operation = int (*)(int, int);

int calculate(int a, int b, Operation operation)
{
    return operation(a, b);
}

int main()
{
    Operation maximum = [](int a, int b) {
        return a > b ? a : b;
    };

    std::cout << calculate(12, 9, maximum) << "\n";
    std::cout << calculate(4, 7, [](int a, int b) {
        return a < b ? a : b;
    }) << "\n";

    return 0;
}

運行結果:

12
4

有捕獲的 Lambda 需要在自身對象中保存捕獲的數據,因此不能轉換爲普通函數指針:

int offset = 10;

// Operation operation = [offset](int a, int b) {
//     return a + b + offset;
// }; // 错误:有捕获的 Lambda 不能转换为普通函数指针

有關捕獲列表和 Lambda 類型的完整解釋,見後面的 Lambda 表達式章節。

擴展示例5:使用 static_cast 選擇重載函數

static_cast<目标类型>(表达式) 是 C++ 的顯式類型轉換語法。取得重載函數地址時,可以用它明確告訴編譯器需要哪個重載版本。

auto 會讓編譯器根據右側表達式推導變量類型。下面的 decimal_square 實際類型仍然是 double (*)(double)

示例:

#include <iostream>

int square(int value)
{
    return value * value;
}

double square(double value)
{
    return value * value;
}

int main()
{
    int (*integer_square)(int) = square;
    auto decimal_square = static_cast<double (*)(double)>(square);

    std::cout << integer_square(5) << "\n";
    std::cout << decimal_square(1.5) << "\n";

    return 0;
}

運行結果:

25
2.25

總結:優先使用明確的函數指針類型選擇重載;上下文仍不明確時,再使用 static_cast。完整轉換規則見後面的 C++ 類型轉換章節。

擴展示例6:使用 std::function 保存不同的可調用對象

需要的標準版本: C++11。

std::function 是標準庫 <functional> 頭文件提供的通用可調用對象包裝器。它的基本語法是:

std::function<返回值类型(参数类型列表)>

例如,std::function<int(int, int)> 可以保存一個“接收兩個 int 並返回 int”的可調用對象,包括普通函數、函數指針、匹配的 Lambda 和函數對象。

示例:

#include <functional>
#include <iostream>

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

int main()
{
    std::function<int(int, int)> operation = add;
    std::cout << operation(3, 4) << "\n";

    int offset = 100;
    operation = [offset](int a, int b) {
        return a + b + offset;
    };

    std::cout << operation(3, 4) << "\n";

    return 0;
}

運行結果:

7
107

普通函數指針不能保存有捕獲的 Lambda,std::function 可以。但 std::function 使用類型擦除實現統一包裝,存在額外的間接調用開銷,某些情況下還可能發生動態內存分配。因此,不應在所有場景中無條件用它替代函數指針。

完整用法和更多示例見後面的 std::function 章節。

函數指針、Lambda 與 std::function 的選擇

需求推薦方式說明
保存普通函數或無捕獲 Lambda函數指針類型簡單,沒有類型擦除開銷
保存有捕獲 LambdaLambda 自身類型或 std::function普通函數指針無法保存捕獲的數據
接收任意類型並且隻立即調用模板參數通常能保留具體類型並被編譯器優化
在成員變量中統一保存多種回調std::function可以統一包裝多種可調用對象
調用指定對象的非靜態成員函數成員函數指針調用時必須同時提供對象

這裏的“模板參數”和“Lambda 自身類型”需要結合後面的模板及 Lambda 章節理解。初次學習只需記住:函數指針適合保存普通函數和無捕獲 Lambda;需求更復雜時,再考慮其他工具。

常見錯誤

錯誤1:忘記變量名兩側的括號

int (*operation)(int, int); // 正确:函数指针
int *operation(int, int);   // 这是返回 int* 的函数声明

錯誤2:函數類型不匹配

void print(int value);

// int (*operation)(int, int) = print; // 错误:返回值和参数列表不匹配

錯誤3:調用空函數指針

void (*task)() = nullptr;

// task(); // 错误:产生未定义行为

if (task)
{
    task(); // 正确:先检查再调用
}

錯誤4:用普通函數指針保存非靜態成員函數

class Robot
{
public:
    void run();
};

// void (*task)() = &Robot::run;     // 错误
void (Robot::*task)() = &Robot::run; // 正确

錯誤5:調用成員函數指針時沒有提供對象

Robot robot;
void (Robot::*task)() = &Robot::run;

// task();          // 错误:没有对象
(robot.*task)();    // 正确

錯誤6:把有捕獲的 Lambda 轉換爲函數指針

using Operation = int (*)(int);
int offset = 10;

// Operation operation = [offset](int value) {
//     return value + offset;
// }; // 错误

總結

  • 函數指針保存函數地址,類型中包含函數的返回值和參數列表。
  • 函數名通常可以直接賦給簽名兼容的函數指針。
  • 函數指針可以用於回調、運行時選擇函數和函數表。
  • 靜態成員函數可以使用普通函數指針。
  • 非靜態成員函數需要成員函數指針,並且調用時必須提供對象。
  • 對象使用 .*,對象指針使用 ->*
  • const 是成員函數指針類型的一部分。
  • 可能爲空的函數指針必須在調用前檢查。
  • 現代 C++ 可以使用 using 簡化類型,用無捕獲 Lambda 創建函數指針。
  • 需要統一保存多種可調用對象時,可以使用 std::function
音乐页