第 16 節

function pointer

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

function pointer

阅读提示: 本章前半部分只使用已经学过的函数、指针和类,需要按顺序掌握。后半部分会扩展到 nullptrusing、Lambda 和 std::function 等现代 C++ 特性。扩展部分会提前解释新语法和 API 的基本用法,但需要学完后面的“现代 C++”章节才能完全看懂。初次学习时可以先了解结论,暂时跳过扩展部分。

函数指针的基本概念

前面已经学习过:普通指针可以保存变量的地址,通过指针能够间接访问变量。

函数编译后也存放在内存中,因此函数同样有地址。函数指针就是保存函数地址的指针变量。通过函数指针,可以间接调用它所指向的函数。

函数指针常用于:

  • 把一个函数传给另一个函数,也就是回调函数
  • 在程序运行过程中选择不同的处理函数
  • 把多个形式相同的函数放进表中统一管理
  • 保存并调用类的成员函数

假设有下面的函数:

int add(int a, int b);

指向它的函数指针可以声明为:

int (*function_pointer)(int, int);

这条声明可以从变量名向外阅读:

  1. function_pointer 是变量名。
  1. *function_pointer 说明它是一个指针。
  1. (int, int) 说明它指向的函数接收两个 int 参数。
  1. 最前面的 int 说明该函数返回一个 int

函数指针的基本语法为:

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

变量名两侧的括号不能省略:

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

总结:函数指针类型不仅包含返回值类型,还包含完整的参数类型列表。

示例1:保存函数地址并调用函数

函数名在大多数表达式中会自动转换为函数地址,因此可以直接把 add 赋给函数指针。也可以写成 &add,其中 & 表示获取函数地址,两种写法效果相同。

通过函数指针调用函数也有两种写法:

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

Example:

#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:函数指针类型必须匹配

函数指针只能指向类型兼容的函数。返回值类型和每个参数的类型都要匹配。

Example:

#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:运行时切换不同的函数

只要函数类型相同,一个函数指针就可以在不同时间指向不同函数。这可以把“选择执行哪个操作”和“执行操作”分开。

Example:

#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

Example:

#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));

第一种写法更容易阅读,通常建议在教程、头文件和函数定义中保留有意义的参数名。

Example:

#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

Example:

#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:函数重载与函数指针

前面已经学习过函数重载:多个函数可以使用同一个名字,只要参数列表不同。

当重载函数名赋给类型明确的函数指针时,编译器会根据函数指针的类型选择匹配的重载版本。

Example:

#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++ 扩展部分会介绍如何使用显式类型转换进一步指定重载版本。

pointer to member function

普通函数指针与成员函数指针的区别

非静态成员函数必须依附某个对象调用,因为函数内部可能访问该对象的数据成员和 this 指针。因此,非静态成员函数不能保存在普通函数指针中,需要使用成员函数指针

成员函数指针的类型中必须写出它所属的类:

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

取得成员函数地址时必须写 &类名::函数名。调用时还需要提供一个对象:

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

示例8:静态成员函数使用普通函数指针

静态成员函数属于类,但不依附某个具体对象,也没有 this 指针。因此,可以使用普通函数指针保存静态成员函数的地址。

Example:

#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 对象。

运算符 .* 表示“通过对象使用成员指针”。调用表达式外层的括号不能省略,因为函数调用运算符 () 的优先级高于 .*

Example:

#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:通过对象指针调用成员函数指针

如果手中保存的是对象指针,需要使用 ->* 运算符。

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

Example:

#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

Example:

#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调用空函数指针会产生未定义行为,因此可能为空时必须先检查。

Example:

#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 不会创建新的类型,也没有运行时开销,只是让类型更容易阅读。

Example:

#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

Example:

#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 可以转换为签名兼容的普通函数指针。

Example:

#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)

Example:

#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 和函数对象。

Example:

#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 的选择

|Requirement|推荐方式|Explanation|

|:---|:---|:---| |保存普通函数或无捕获 Lambda|function pointer|类型简单,没有类型擦除开销|

|保存有捕获 Lambda|Lambda 自身类型或 std::function|普通函数指针无法保存捕获的数据|

|接收任意类型并且只立即调用|模板参数|通常能保留具体类型并被编译器优化|

|在成员变量中统一保存多种回调|std::function|可以统一包装多种可调用对象|

|调用指定对象的非静态成员函数|pointer to member function|调用时必须同时提供对象|

这里的“模板参数”和“Lambda 自身类型”需要结合后面的模板及 Lambda 章节理解。初次学习只需记住:函数指针适合保存普通函数和无捕获 Lambda;需求更复杂时,再考虑其他工具。

Common Errors

错误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
音乐页