跳到正文

Wiki

函式指標

約 10 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

1.函式指標

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

1.1.函式指標的基本概念

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

函式編譯後也存放在記憶體中,因此函式同樣有地址。函式指標就是儲存函式地址的指標變數。通過函式指標,可以間接呼叫它所指向的函式。

函式指標常用於:

  • 把一個函式傳給另一個函式,也就是回撥函式
  • 在程式執行過程中選擇不同的處理函式
  • 把多個形式相同的函式放進表中統一管理
  • 儲存並呼叫類的成員函式

假設有下面的函式:

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.2.示例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(...) 呼叫,程式碼更簡潔。

1.3.示例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

注意:不要依賴引數呼叫時可能發生的隱式型別轉換。函式本身的型別必須與函式指標型別相容。

1.4.示例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

1.5.示例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 保持不變,只通過傳入不同函式來改變計算行為。

1.6.示例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

1.7.示例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,但使用下標前必須檢查範圍。

1.8.示例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++ 擴充套件部分會介紹如何使用顯式型別轉換進一步指定過載版本。

2.成員函式指標

2.1.普通函式指標與成員函式指標的區別

非靜態成員函式必須依附某個物件呼叫,因為函式內部可能訪問該物件的資料成員和 this 指標。因此,非靜態成員函式不能儲存在普通函式指標中,需要使用成員函式指標

成員函式指標的型別中必須寫出它所屬的類:

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

取得成員函式地址時必須寫 &类名::函数名。呼叫時還需要提供一個物件:

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

2.2.示例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

2.3.示例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

同一個成員函式指標配合不同物件呼叫時,會訪問各個物件自己的資料。

2.4.示例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

總結:物件使用 .*,物件指標使用 ->*

2.5.示例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 成员函数

3.現代 C++ 擴充套件(可選讀)

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

3.1.擴充套件示例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 章節。

3.2.擴充套件示例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.3.擴充套件示例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 起別名,再作為引數型別或返回值型別。

3.4.擴充套件示例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 表示式章節。

3.5.擴充套件示例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++ 型別轉換章節。

3.6.擴充套件示例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 章節。

3.7.函式指標、Lambda 與 std::function 的選擇

需求 推薦方式 說明
儲存普通函式或無捕獲 Lambda 函式指標 型別簡單,沒有型別擦除開銷
儲存有捕獲 Lambda Lambda 自身型別或 std::function 普通函式指標無法儲存捕獲的資料
接收任意型別並且隻立即呼叫 模板引數 通常能保留具體型別並被編譯器最佳化
在成員變數中統一儲存多種回撥 std::function 可以統一包裝多種可呼叫物件
呼叫指定物件的非靜態成員函式 成員函式指標 呼叫時必須同時提供物件

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

4.常見錯誤

4.1.錯誤1:忘記變數名兩側的括號

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

4.2.錯誤2:函式型別不匹配

void print(int value);

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

4.3.錯誤3:呼叫空函式指標

void (*task)() = nullptr;

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

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

4.4.錯誤4:用普通函式指標儲存非靜態成員函式

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

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

4.5.錯誤5:呼叫成員函式指標時沒有提供物件

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

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

4.6.錯誤6:把有捕獲的 Lambda 轉換為函式指標

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

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

5.總結

  • 函式指標儲存函式地址,型別中包含函式的返回值和引數列表。
  • 函式名通常可以直接賦給簽名相容的函式指標。
  • 函式指標可以用於回撥、執行時選擇函式和函式表。
  • 靜態成員函式可以使用普通函式指標。
  • 非靜態成員函式需要成員函式指標,並且呼叫時必須提供物件。
  • 物件使用 .*,物件指標使用 ->*
  • const 是成員函式指標型別的一部分。
  • 可能為空的函式指標必須在呼叫前檢查。
  • 現代 C++ 可以使用 using 簡化型別,用無捕獲 Lambda 建立函式指標。
  • 需要統一儲存多種可呼叫物件時,可以使用 std::function