C++ 函数指针详解:从基础到高级应用

在《指针简介》中,你已了解到:指针是一种保存另一变量地址的变量。函数指针与之类似,只不过它们指向的不是变量,而是函数。

请观察以下函数:

int foo()
{
    return 5;
}

标识符 foo() 是这个函数的名称。但函数本身是什么类型呢?函数拥有各自的函数类型——本例中,它是一个返回整型且无参数的函数类型。与变量一样,函数在内存中也占据一个已分配的地址,因此是左值。

当使用调用运算符 () 调用函数时,程序执行会跳转到被调函数的地址:

int foo()    // foo 的代码起始地址为 0x002717f0
{
    return 5;
}

int main()
{
    foo();   // 跳转到地址 0x002717f0

    return 0;
}

在你的编程生涯中(如果你尚未遇到),很可能会犯这样一个低级错误:

#include <iostream>

int foo()    // 代码起始地址为 0x002717f0
{
    return 5;
}

int main()
{
    std::cout << foo << '\n';   // 本想调用 foo() 并打印返回值,结果却把函数本身打印了!
    
    return 0;
}

我们并未调用 foo() 并打印其返回值,而是无意中将函数 foo 直接传给了 std::ostream。此时会发生什么?

当函数名在表达式中出现且其后不跟括号时,C++ 会将其转换为函数指针(即保存该函数地址的指针)。随后 operator<< 试图打印该函数指针;由于 std::ostream 并不知道如何打印函数指针,标准规定此时应将函数指针转换为 booloperator<< 能够打印)。鉴于该指针非空,因而总是被转换为布尔值 true,于是输出:

1

提示
某些编译器(如 Visual Studio)提供了扩展,直接打印函数地址:

0x002717f0

如果你的目标平台不打印函数地址而你想查看,可强制将其转换为 void* 并打印:

std::cout << reinterpret_cast<void*>(foo) << '\n';

此行为由实现定义,并非所有平台均支持。

正如可以为普通变量声明非常量指针一样,我们也可以声明指向函数的非常量指针。下文将详细探讨函数指针及其用途。函数指针属于进阶主题,若你仅需 C++ 基础,可跳过或略读本节剩余内容。

指向函数的指针

声明非常量函数指针的语法堪称 C++ 中最丑陋的语法之一:

// fcnPtr 是一个指向“无参数且返回 int”的函数的指针
int (*fcnPtr)();

上例中,fcnPtr 可指向任何与此签名匹配的函数。

*fcnPtr 两侧的括号必不可少:若写成 int* fcnPtr(),则会被解释为“声明一个名为 fcnPtr、无参数且返回 int* 的函数”。

若要声明常量函数指针,需把 const 放在星号之后:

int (*const fcnPtr)();

若将 const 放在 int 之前,则意味着所指向的函数返回 const int

提示
函数指针语法难以阅读。下列两篇文章提供了系统的解析方法:

  1. 顺时针/螺旋规则(Clockwise/Spiral Rule)
  2. C 右-左规则(Right-Left Rule)

将函数绑定到函数指针

函数指针可在初始化时绑定函数,非常量函数指针亦可在之后重绑定。与变量指针类似,可使用 &foo 取得函数地址:

int foo() { return 5; }
int goo() { return 6; }

int main()
{
    int (*fcnPtr)() { &foo };   // fcnPtr 指向 foo
    fcnPtr = &goo;              // fcnPtr 改指 goo

    return 0;
}

常见错误:

fcnPtr = goo();   // 错误!把函数返回值(int)赋给函数指针

正确的做法是不加括号,以取得函数地址而非调用函数。

函数指针的类型(参数及返回类型)必须与目标函数完全匹配:

// 函数原型
int foo();
double goo();
int hoo(int x);

// 初始化
int (*fcnPtr1)(){ &foo };      // 正确
int (*fcnPtr2)(){ &goo };      // 错误——返回类型不符
double (*fcnPtr4)(){ &goo };   // 正确
fcnPtr1 = &hoo;                // 错误——参数列表不符
int (*fcnPtr3)(int){ &hoo };   // 正确

与一般类型不同,C++ 会在需要时隐式将函数转换为函数指针(因此无需显式使用 &)。然而,函数指针与 void* 之间不存在隐式转换(尽管某些编译器如 Visual Studio 可能允许)。

int (*fcnPtr5)() { foo };   // 正确,foo 隐式转换为函数指针
void* vPtr { foo };         // 错误,尽管某些编译器允许

函数指针也可用 nullptr 初始化或赋值:

int (*fcnptr)() { nullptr };

通过函数指针调用函数

有两种方式:

  1. 显式解引用

    int foo(int x) { return x; }
    
    int main()
    {
        int (*fcnPtr)(int){ &foo };
        (*fcnPtr)(5);   // 通过 fcnPtr 调用 foo(5)
        return 0;
    }
    
  2. 隐式解引用

    fcnPtr(5);        // 与上式效果相同
    

隐式解引用与普通函数调用写法一致,因为普通函数名本质上就是函数指针。但部分旧编译器不支持隐式解引用。

函数指针亦可为 nullptr,故调用前应检查:

if (fcnPtr)
    fcnPtr(5);

函数指针调用不支持默认实参(进阶)

对带默认实参的普通函数,编译器在编译期会把缺省参数补全。然而,通过函数指针的调用在运行期解析,因此不会补全默认实参。

关键洞见
由于解析发生在运行期,函数指针调用不会应用默认实参。

这意味着可利用函数指针消除因默认实参引起的重载歧义:

void print(int x)              { std::cout << "print(int)\n"; }
void print(int x, int y = 10)  { std::cout << "print(int, int)\n"; }

int main()
{
    // print(1); // 调用歧义

    using vnptr = void(*)(int);
    vnptr pi { print };  // 指向 print(int)
    pi(1);               // 明确调用 print(int)

    // 或使用 static_cast
    static_cast<void(*)(int)>(print)(1);
}

将函数作为参数传递给其他函数

函数指针最常见的用途之一便是把函数作为实参传给另一函数。这类被传入的函数常称“回调函数”。

设想你需要编写排序数组的函数,但又希望用户决定排序方式(升序或降序)。以下以选择排序为例,展示如何实现:

原选择排序算法核心是比较相邻元素。若把比较逻辑抽象为函数,即可由用户注入:

bool ascending(int x, int y) { return x > y; }   // 升序比较

随后改造 SelectionSort,使其接受一个函数指针参数:

void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int));

完整示例:

#include <utility>
#include <iostream>

void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))
{
    if (!array || !comparisonFcn) return;

    for (int startIndex{ 0 }; startIndex < size - 1; ++startIndex)
    {
        int bestIndex{ startIndex };
        for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
            if (comparisonFcn(array[bestIndex], array[currentIndex]))
                bestIndex = currentIndex;
        std::swap(array[startIndex], array[bestIndex]);
    }
}

bool ascending(int x, int y)  { return x > y; }
bool descending(int x, int y) { return x < y; }

void printArray(int* array, int size)
{
    for (int i{ 0 }; i < size; ++i) std::cout << array[i] << ' ';
    std::cout << '\n';
}

int main()
{
    int array[9]{ 3,7,9,5,6,1,8,2,4 };

    selectionSort(array, 9, descending);
    printArray(array, 9);

    selectionSort(array, 9, ascending);
    printArray(array, 9);
}

输出:

9 8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8 9

用户也可自定义更复杂的比较函数,例如“偶数优先”:

bool evensFirst(int x, int y)
{
    if ((x % 2 == 0) && !(y % 2 == 0)) return false;
    if (!(x % 2 == 0) && (y % 2 == 0)) return true;
    return ascending(x, y);
}

注意
如果函数形参本身就是函数类型,则会被隐式转换为指向该函数的指针。因此:

void selectionSort(int*, int, bool (*)(int, int));

等价于

void selectionSort(int*, int, bool(int, int));

该规则仅适用于函数形参;在非形参位置,后者会被视为前向声明。

提供默认函数

若允许用户传入比较函数,通常可为其预置常用实现,如升序、降序:

void selectionSort(int* array, int size,
                   bool (*comparisonFcn)(int, int) = ascending);

只要用户以常规语法调用 selectionSortcomparisonFcn 将默认使用 ascending。需确保 ascending 在此之前已声明。

使用类型别名简化语法

函数指针语法晦涩,可用类型别名改善可读性:

using ValidateFunction = bool(*)(int, int);

随后即可:

bool validate(int x, int y, ValidateFunction pfcn);

使用 std::function

另一种定义函数指针的方法是使用标准库 <functional> 中的 std::function

#include <functional>

bool validate(int x, int y, std::function<bool(int, int)> fcn);

模板实参内写返回类型及形参列表;无形参时括号可省。

示例:

#include <functional>
#include <iostream>

int foo() { return 5; }
int goo() { return 6; }

int main()
{
    std::function<int()> fcnPtr{ &foo };
    fcnPtr = &goo;
    std::cout << fcnPtr() << '\n';

    std::function fcnPtr2{ &foo };   // 使用 CTAD 推断模板实参
}

建议对多次出现的函数指针类型创建别名:

using ValidateFunction = std::function<bool(int, int)>;

注意:定义别名时必须显式模板实参,无法使用 CTAD。

函数指针的类型推断

auto 推断变量类型一样,也可用其推断函数指针类型:

auto fcnPtr{ &foo };

语法简洁,但隐藏了函数签名细节,易出错。

结论

函数指针主要用于:

  1. 把函数存入数组或其他数据结构;
  2. 把函数作为参数传递给另一函数。

由于原生语法复杂且易错,推荐使用 std::function

  • 若某函数指针类型仅在一处使用,std::function 可直接书写;
  • 若多次使用,应创建 std::function 的类型别名以防重复。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"