惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
罗磊的独立博客
腾讯CDC
云风的 BLOG
云风的 BLOG
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
U
Unit 42
I
InfoQ
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
美团技术团队
IT之家
IT之家
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
GbyAI
GbyAI
S
SegmentFault 最新的问题

SamHou's Blog

tailscale 进阶用法 一个服务绑一个域名并启用 SSL 水星冲浪日志 5 傲娇、后宫和党争——零之使魔动画点评 人生的意义是什么?——脏翅膀点评 水星冲浪日志 4 总感觉有什么不对——Anemoi 全线点评 Hexo 博客接入 Fediverse —— Hatsu + Vercel 踩坑记 水星冲浪日志 3 —— Fediverse、Arch Linux 和写作风格 DroidSpaces 在安卓上跑 Linux 发行版踩坑实录 从零开始配置 VPS —— 主机名、用户组和远程权限安全实践 奶奶都能看懂的 C++ —— 类、初始化、预处理和分离式编译 让你验证请求来自 CF —— Authenticated Origin Pulls 攻略 水星冲浪日志 2 —— PT、换域名、约稿和建站机 奶奶都能看懂的 C# —— LINQ、 Lambda 和 IEnumerable
奶奶都能看懂的 C++ —— 函数指针、decltype、类型别名和尾置返回
Sam Hou · 2026-03-04 · via SamHou's Blog

上一节我们讲了函数,这次来聊聊函数指针

顾名思义,函数指针指的就是,指向函数的指针。也就是,指针解引用之后是一个可调用的函数。

先来看一段示例代码:

1
2
3
4
5
6
7
8
int addInt(int a, int b) {
return a + b;
}
int main() {
int (*pAddInt)(int, int);
pAddInt = addInt;
cout << pAddInt(3, 4) << endl; // 7
}

现在我们来仔细看看这段代码做了些什么。

声明函数指针

首先我们声明并定义了一个函数 addInt,它接受两个 int 类型的参数,然后返回它们的和。

然后我们来看今天的重点,main 函数。

1
int (*pAddInt)(int, int);

这就是我们所说的,函数指针的声明

我们来看看为什么它是函数指针:

  • pAddInt 是这个变量的名称,我们从这里往外读
  • 首先是括号,里面有一个 *,说明它是个指针
  • 右侧的括号中有两个 int 类型,说明这是个参数列表
  • 左侧的 int 表示一个返回值类型
  • 因此,它是一个函数指针,可以指向一个函数,这个函数需要接受 2 个 int 类型的参数,并返回一个 int

正如我们在介绍指针时所提到的,指针所指向的内容是有类型限定的,由指针的类型决定。函数指针也是如此,你看,上面的代码已经明确了允许指向的函数的要求了。

其实,函数指针就是函数加个星号而已,声明和正常的指针没什么差异:

1
2
int addInt(int, int);
int (*pAddInt)(int, int);

再提醒你一下,在 C++ 中,括号的优先级是比 * 更高的,因此,声明函数指针的时候,必须给星号加上括号。比如,下面的声明是错误的,会声明一个返回值指向 int 类型的指针函数,而非函数指针

1
int *pAddInt(int, int);

是不是看到了指向数组指针的影子?是的,由于括号问题,它们呈现出相似的视觉效果。

好了,现在我们声明了一个函数指针,但问题是,这个指针现在没有指向任何东西,行为是未定义的。

赋值函数指针

我们来看下一行:

1
pAddInt = addInt;

这一行代码将函数指针 pAddInt 指向了一个实际的函数 addInt

但是你或许注意到了一个问题——指针指向的是一个地址,但是为什么这里没有取地址呢?

首先我们明确一点,函数确实有一个地址,指针也正是指向了这个内存地址。指针本身存储了地址,是一个对象,但是函数并不是对象(也不能赋值给变量)。

然后我们再来看为什么没有取地址符号 & 这个问题。

其实你自己先试一下就会发现,加上这个符号也能通过编译,并正常运行:

1
pAddInt = &addInt;

这种灵活来自于 C++ 的退化性质,当一个表达式中存在函数名字的时候,这个函数会自动转换为函数指针

是不是很耳熟?没错,这和数组是一个道理!还记得吗,我们在数组与指针这一节中,曾经提到过,数组在表达式中也会自动转换为指针,指向的是第一个元素。

函数也是类似,函数名字在表达式中自动转换为指向该函数的函数指针。因此,取地址符号是可以省略的

调用函数指针

既然我们已经有了一个函数指针,是时候来看看怎么用了。

1
cout << pAddInt(3, 4) << endl;

看到了吗?我们正在调用函数指针,正如调用普通的函数一样。

我知道你要说什么。你肯定想这么干:

1
cout << (*pAddInt)(3, 4) << endl;

你自己试试就会发现无论是否解引用,表现都是一致的。

这是因为,编译器为你承担起了这一切——和自动退化相对称,解引用也并不是必须的,我愿称之为一组对称法则(提示,这不是官方说法,是我个人的总结)。

实际上,函数指针可以作为返回值和参数,就如通常的指针一样。但是,在继续讨论函数指针前,我们先来讲解几个好用的东西,然后再继续深入。

当我们涉及指针的时候,是不是发现声明语句越来越复杂了呢?一个变量的类型可能变得相当复杂:

1
2
3
4
int* addInt(int *a, int *b) {
*a = *a + *b;
return a;
}

要创建指向上面函数的指针,我们必须使用这种类型声明语句:

1
int* (*pAddInt)(int*, int*);

有没有什么简单的方法呢?当然,让我们隆重介绍 decltype

看看这个:

1
decltype(addInt) *pAddInt;

decltype 可以把一个名字对应的类型,直接偷过来!我们这里偷来了 addInt 这个函数的类型(包括参数列表类型和返回值)。通常情况下,这样会声明一个新的函数,但由于我们加上了一个 *,所以我们现在声明的是函数指针。

一个非常重要的事情是,decltype 不发生退化,所以记得带上 *。decltype 只是负责推算里面表达式的类型而已,不会帮你转换。

因此,使用 decltype 可以极大简化代码编写流程,让我们免去书写指针的类型的麻烦。

注意这个当然不局限于函数指针,而是对于任何指针都是有效的!

比如,指向数组的指针:

1
2
3
int a[10];
decltype(a) *p;
int (*p2)[10];

第二、三行声明的指针,类型都是一样的,都是指向含有 10 个 int 的数组的指针哦。

类型别名

明白了 decltype 能够偷来类型,我们再来讲一个能够把类型起个别名的东西,类型别名

类型别名,从名字上来看,就是用另外一个名字,替代原来的类型名字。

typedef

传统的类型别名使用方法是 typedef,比如:

1
2
3
typedef double d;
d num = 3.14;
cout << num << endl;

我们给 double 起了个别名叫做 d。

但这个太简单了,当类型变得非常复杂的时候,别名才会发挥出作用:

1
2
3
4
typedef double** d; // 指向指针的指针类型
typedef int arrType[10]; // 包含10个int的数组类型
typedef int (*arrType)[10]; // 一个指针类型,指向的是包含10个int的数组类型
typedef int *arrType[10]; // 包含10个指向int的指针的数组类型

你现在可能非常疑惑,为什么第一条和后面三条的语法看起来完全不一样(你看看,第一句的语法好像是 typedef 类型 新的名字,但后面的语法完全不是这样)?换句话说,typedef 到底是如何工作的?

别被迷惑了,让我们先把 typedef 本身移除掉——

1
2
3
4
double** d; // 指向指针的指针类型
int arrType[10]; // 包含10个int的数组类型
int (*arrType)[10]; // 一个指针类型,指向的是包含10个int的数组类型
int *arrType[10]; // 包含10个指向int的指针的数组类型

现在你可以看出些端倪了:给类型取别名,和创建变量的本质没什么区别。

奶奶都知道可以这么创建变量:

1
2
3
4
double** dp;
int arr[10];
int (*arr)[10];
int *arr[10];

嗯?是不是发现了什么呢?语法完全一致

也就是说,typedef 和创建变量的区别只有一个,就是前者创建的名字是一个可以直接使用的类型

比如:

1
2
d myd;
arrType myArr; // 直接当作类型使用!

好了,既然你已经知道如何创建类型别名,那么我们回到函数指针。同理,你可以这么创建函数指针类型:

1
2
typedef int (*funcPType)(int*, int*);
funcPType p1; // 这是一个函数指针

这样可以节省大量时间。

using

在新版的 C++ 中,你可以用另一种方式创建类型别名:

1
2
3
using d = double;
d d1 = 3.14;
cout << d1 << endl; // 3.14

这个 using 会把等号后面的类型,起别名,别名名字为等号前的内容。我们可以把上面的 typedef 全部转换为 using:

1
2
3
4
using d = double** ; // 指向指针的指针类型
using arrType = int[10]; // 包含10个int的数组类型
using arrType = int (*)[10]; // 一个指针类型,指向的是包含10个int的数组类型
using arrType = int*[10]; // 包含10个指向int的指针的数组类型

很简单吧?只是把名字前置了,相比 typedef,这种方式看起来更加方便。

当然,也别忘了我们的主角函数指针:

1
using funcPType = int (*)(int*, int*);

更强大的是,你可以把类型别名和 decltype 组合使用,避免写出复杂的类型:

1
2
using funcType = decltype(func); // 现在,你给函数起了别名
funcType funcP = *funcType; // 根据函数别名,创建对应的函数指针

传递函数指针

既然函数指针本身,是个指针,那么它就变成了一个对象,自然可以传递,就如其它指针一样。

作为参数传递

那这有什么用呢?其实,这样可以让同一个函数根据传入的函数指针,执行不同的操作。

比如,下面的代码,将运算的函数指针传入,然后调用函数执行自定义操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sum(int a, int b) {
return a + b;
}
int diff(int a, int b) {
return abs(a - b);
}
using funcPType = decltype(sum)*;
int mathOperation(int a, int b, funcPType funcP) {
return funcP(a, b);
}
int main() {
int a = 3, b = 4;
cout << mathOperation(a, b, sum) << endl; // 7
cout << mathOperation(a, b, diff) << endl; // 1
}

花点时间好好理解一下上面的代码。

首先,写了两个执行算术操作的函数。

然后,我们创建了一个叫做 funcPType 的类型别名,它代表的类型是一个函数指针,指向接受 2 个 int 参数的返回 int 的函数。

之后,写了一个算术操作的函数,接受函数指针类型,在内部调用这个函数指针,执行相对应的操作。

最后写了 main 函数,两次传入不同的函数名字。函数名自动退化为函数指针,作为参数传入。你可以看到,我们的 a b 都没有变化,但是结果却不同。这正是因为传入了不同的函数指针导致的。

也就是说,函数指针可以把不同操作打包,交给其它部分操作。也就是,把行为作为数据传递。函数指针只记下了传入什么、返回什么,不关心实际执行了什么操作——也正因为如此,我们可以在需要函数指针的地方传入不同的函数,这极大地提升了我们程序的灵活性。

另外我要提醒你一点,不要给函数名字后面加上括号。这样会变成直接调用函数,传递的就是返回值了,会发生类型不匹配直接报错。我们要传递的,是会自动退化成函数指针的函数名字

作为返回值

不止参数。函数指针也可以作为返回值

来看看下面的示例代码(略去和上述相同的 sum 和 diff 函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using funcPType = decltype(sum)*;
funcPType chooseOperation(char c) {
if(c == '+') return sum;
if(c == '-') return diff;
return nullptr;
}
int main() {
char c1 = '+';
char c2 = '-';
funcPType choice = chooseOperation(c1);
cout << choice(1, 3) << endl; // 4
choice = chooseOperation(c2);
cout << choice(1, 3) << endl; // 2
}

仔细看看,我们的函数根据传入的参数不同,返回了不同的函数指针。返回的指针可以作为函数调用——在两条输出语句那里,我们用的是同一个函数指针名称,但是它们指向的是不同的函数,也就导致了不同的执行结果。

这是一个非常现实的例子,同样也是把行为作为数据传递的体现。掌握了这种写法,你就可以大大提升你的程序的灵活度。

此外,我们也可以不用类型别名,但是这会出现一些非常复杂的东西,我们在下面的尾置返回类型中,进行进一步的探讨。

尾置返回类型

关于函数指针我们说的够多了,现在我们再来补充一点 C++ 新版本的知识,尾置返回类型

顾名思义,尾置返回类型允许我们把函数返回值的类型放在末尾。来看看这个:

1
2
3
4
5
auto chooseOperation(char c) -> funcPType {
if(c == '+') return sum;
if(c == '-') return diff;
return nullptr;
}

这是上面的代码的另一种写法。原本用于声明函数返回类型的首位被 auto 代替了,真正的返回类型写在最后。

你可能在想,这不是多此一举吗?其实,只有当你不想用类型别名,但是却想返回函数指针(或任何复杂的类型,比如指向数组的指针)的时候,你会发现这大大被简化了:

1
2
3
4
5
6
7
int (*chooseOperation(char c))(int, int) // 一般情况
auto chooseOperation(char c) -> int(*)(int, int) // 尾置返回类型,这下看懂了
{
if(c == '+') return sum;
if(c == '-') return diff;
return nullptr;
}

也补充一下,如果不用尾置返回类型,第一行

1
int (*chooseOperation(char c))(int, int)

的含义是:

  • 从内向外阅读。首先 chooseOperation(char c) 说明此处是个函数。
  • 其次 (*...) 表示它的返回值是个指针。
  • 再次,int(...)(int,int) 表示这个指针指向的是个函数,函数接受 2 个 int,返回一个 int。

如果你有些混乱,不妨翻到文章开头,看看函数指针是如何声明的,对比看看:

1
2
int (*pAddInt)(int, int);
int (*chooseOperation(char c))(int, int)

现在是不是发现了点规律?只是把变量名字换成函数名字+参数列表而已。

但是,我完全不建议你这么写。请一定要使用尾置返回类型、类型别名和 decltype,让你的代码更加可读。

关于函数指针和简化代码的方法,我们已经涉及了相当深入的内容了。下一节,我们将离开函数这一篇章,进入新的一部分——