在《指针简介》中,你已了解到:指针是一种保存另一变量地址的变量。函数指针与之类似,只不过它们指向的不是变量,而是函数。
请观察以下函数:
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
并不知道如何打印函数指针,标准规定此时应将函数指针转换为 bool
(operator<<
能够打印)。鉴于该指针非空,因而总是被转换为布尔值 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
。
提示
函数指针语法难以阅读。下列两篇文章提供了系统的解析方法:
将函数绑定到函数指针
函数指针可在初始化时绑定函数,非常量函数指针亦可在之后重绑定。与变量指针类似,可使用 &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 };
通过函数指针调用函数
有两种方式:
显式解引用
int foo(int x) { return x; } int main() { int (*fcnPtr)(int){ &foo }; (*fcnPtr)(5); // 通过 fcnPtr 调用 foo(5) return 0; }
隐式解引用
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);
只要用户以常规语法调用 selectionSort
,comparisonFcn
将默认使用 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 };
语法简洁,但隐藏了函数签名细节,易出错。
结论
函数指针主要用于:
- 把函数存入数组或其他数据结构;
- 把函数作为参数传递给另一函数。
由于原生语法复杂且易错,推荐使用 std::function
:
- 若某函数指针类型仅在一处使用,
std::function
可直接书写; - 若多次使用,应创建
std::function
的类型别名以防重复。