news 2026/4/15 8:14:52

函数—C++的编程模块(函数指针)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
函数—C++的编程模块(函数指针)

函数指针

如果未提到函数指针,则对C 或C++函数的讨论将是不完整的。我们将大致介绍一下这个主题,将完
整的介绍留给更高级的图书。

与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地
址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的
地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,
这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的
函数。

函数指针的基础知识

首先通过一个例子来阐释这一过程。假设要设计一个名为estimate( )的函数,估算编写指定行数的代码
所需的时间,并且希望不同的程序员都将使用该函数。对于所有的用户来说,estimate( )中一部分代码都是
相同的,但该函数允许每个程序员提供自己的算法来估算时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给estimate( )。为此,必须能够完成下面的工作:

  • 获取函数的地址;
  • 声明一个函数指针;
  • 使用函数指针来调用函数。

获取函数的地址

获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。也就是说,如果think( )是一个函数,
则think 就是该函数的地址。要将函数作为参数进行传递,必须传递函数名。一定要区分传递的是函数的
地址还是函数的返回值:

process(think); thougtht(think);

process( )调用使得process( )函数能够在其内部调用think( )函数。thought( )调用首先调用think( )函数,
然后将think( )的返回值传递给thought( )函数。

声明函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须
指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,
声明应像函数原型那样指出有关函数的信息。例如,假设Pam leCoder 编写了一个估算时间的函数,其原
型如下:

double pam(int);

则正确的指针类型声明如下:

double (*pf)(int);

这与pam( )声明类似,这是将pam 替换为了(*pf)。由于pam 是函数,因此(*pf)也是函数。而如果
(*pf)是函数,则pf 就是函数指针。

提示:通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换
函数名。这样pf 就是这类函数的指针。

为提供正确的运算符优先级,必须在声明中使用括号将pf 括起。括号的优先级比运算符高,因此*pf
(int)意味着pf( )是一个返回指针的函数,而(*pf)(int)意味着pf 是一个指向函数的指针:

double (*pf)(int); double *pf(int);

正确地声明pf 后,便可以将相应函数的地址赋给它:

double pam(int); double (*pf)(int); pf=pam;

注意,pam( )的特征标和返回类型必须与pf 相同。如果不相同,编译器将拒绝这种赋值:

double ned(double); int ted(int); double (*pt)(int); pf=ned; pf=ted;

现在回过头来看一下前面提到的estimate( )函数。假设要将将要编写的代码行数和估算算法(如pam( )
函数)的地址传递给它,则其原型将如下:

void estimate(int lines,double (*pf)(int));

上述声明指出,第二个参数是一个函数指针,它指向的函数接受一个int 参数,并返回一个double 值。
要让estimate( )使用pam( )函数,需要将pam( )的地址传递给它:

estimate(50,pam);

显然,使用函数指针时,比较棘手的是编写原型,而传递地址则非常简单。

使用指针来调用函数

现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。前面讲过,(*pf)扮演的
角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:

double pam(int); double (*pf)(int); pf=pam; double x=pam(4); double y=(*pf)(5);

实际上,C++也允许像使用函数名那样使用pf:

double y=pf(5);

第一种格式虽然不太好看,但它给出了强有力的提示—代码正在使用函数指针。

函数指针示例

程序清单7.18 演示了如何使用函数指针。它两次调用estimate( )函数,一次传递betsy( )函数的地址,
另一次则传递pam( )函数的地址。在第一种情况下,estimate( )使用betsy( )计算所需的小时数;在第二种情
况下,estimate( )使用pam( )进行计算。这种设计有助于今后的程序开发。当Ralph 为估算时间而开发自己
的算法时,将不需要重新编写estimate( )。相反,他只需提供自己的ralph( )函数,并确保该函数的特征标
和返回类型正确即可。当然,重新编写estimate( )也并不是一件非常困难的工作,但同样的原则也适用于更
复杂的代码。另外,函数指针方式使得Ralph 能够修改estimate( )的行为,虽然他接触不到estimate( )的源
代码。

#include <iostream> double betsy(int); double pam(int); void estimate(int lines, double (*pf)(int)); int main() { using namespace std; int code; cout << "How many lines of code do you need?"; cin >> code; cout << "Here's Betsy's estimate:\n"; estimate(code, betsy); cout << "Here's Pam's estimate:\n"; estimate(code, pam); return 0; } double betsy(int lns) { return 0.05 * lns; } double pam(int lns) { return 0.03 * lns + 0.0004 * lns * lns; } void estimate(int lines, double(*pf)(int)) { using namespace std; cout << lines << "lines will take"; cout << (*pf)(lines) << "hour(s)\n"; }

深入探讨函数指针

函数指针的表示可能非常恐怖。下面通过一个示例演示使用函数指针时面临的一些挑战。首先,下面
是一些函数的原型,它们的特征标和返回类型相同:

const double *f1(const double ar[],int n); const double *f2(const double[],int); const double *f3(const double *,int);

这些函数的特征标看似不同,但实际上相同。首先,前面说过,在函数原型中,参数列表const double
ar [ ]与const double * ar 的含义完全相同。其次,在函数原型中,可以省略标识符。因此,const double ar [ ]
可简化为const double [ ],而const double * ar 可简化为const double *。因此,上述所有函数特征标的含义
都相同。另一方面,函数定义必须提供标识符,因此需要使用const double ar [ ]或const double * ar。

接下来,假设要声明一个指针,它可指向这三个函数之一。假定该指针名为pa,则只需将目标函数原
型中的函数名替换为(*pa):

const double *(*p1)(const double * int);

可在声明的同时进行初始化:

const double *(*p1)(const double *,int)=f1;

使用C++11 的自动类型推断功能时,代码要简单得多:

auto p2=f2;

现在来看下面的语句:

cout<<(*p1)(av,3)<<":"<<*(*p1)(av,3)<<endl; cout<<p2(av,3)<<":"<<*p2(av,3)<<endl;

根据前面介绍的知识可知,(p1) (av, 3)和p2(av, 3)都调用指向的函数(这里为f1()和f2()),并将av 和
3 作为参数。因此,显示的是这两个函数的返回值。返回值的类型为const double(即double 值的地址),
因此在每条cout 语句中,前半部分显示的都是一个double 值的地址。为查看存储在这些地址处的实际值,
需要将运算符
应用于这些地址,如表达式
(p1)(av,3)和p2(av,3)所示。

鉴于需要使用三个函数,如果有一个函数指针数组将很方便。这样,将可使用for 循环通过指针依次
调用每个函数。如何声明这样的数组呢?显然,这种声明应类似于单个函数指针的声明,但必须在某个地
方加上[3],以指出这是一个包含三个函数指针的数组。问题是在什么地方加上[3],答案如下(包含初始化):

const double *(*pa[3])(const double *,int)={f1,f2,f3};

为何将[3]放在这个地方呢?pa 是一个包含三个元素的数组,而要声明这样的数组,首先需要使用pa[3]。
该声明的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明pa 是一个
包含三个指针的数组。上述声明的其他部分指出了每个指针指向的是什么:特征标为const double *, int,
且返回类型为const double *的函数。因此,pa 是一个包含三个指针的数组,其中每个指针都指向这样的函
数,即将const double *和int 作为参数,并返回一个const double *。

这里能否使用auto 呢?不能。自动类型推断只能用于单值初始化,而不能用于初始化列表。但声明数
组pa 后,声明同样类型的数组就很简单了:

auto pb=pa;

本书前面说过,数组名是指向第一个元素的指针,因此pa 和pb 都是指向函数指针的指针。
如何使用它们来调用函数呢?pa[i]和pb[i]都表示数组中的指针,因此可将任何一种函数调用表示法用
于它们:

const double *px=pa[0](av,3); const double *py=(*pb[1])(av,3);

要获得指向的double 值,可使用运算符*:

double x=*pa[0](av,3); double y=*(*pb[1])(av,3);

可做的另一件事是创建指向整个数组的指针。由于数组名pa 是指向函数指针的指针,因此指向数组的
指针将是这样的指针,即它指向指针的指针。这听起来令人恐怖,但由于可使用单个值对其进行初始化,
因此可使用auto:

auto pc=&pa;

如果您喜欢自己声明,该如何办呢?显然,这种声明应类似于pa 的声明,但由于增加了一层间接,因
此需要在某个地方添加一个*。具体地说,如果这个指针名为pd,则需要指出它是一个指针,而不是数组。
这意味着声明的核心部分应为(pd)[3],其中的括号让标识符pd 与先结合:

*pd[3] (*pd)[3]

换句话说,pd 是一个指针,它指向一个包含三个元素的数组。这些元素是什么呢?由pa 的声明的其
他部分描述,结果如下:

const double *((*pd)[3])(const double *,int)=&pa;

要调用函数,需认识到这样一点:既然pd 指向数组,那么*pd 就是数组,而(pd)[i]是数组中的元素,
即函数指针。因此,较简单的函数调用是(pd)i,而(pd)i是返回的指针指向的值。也可以
使用第二种使用指针调用函数的语法:使用(
(pd)[i])(av,3)来调用函数,而(
(*pd)[i])(av,3)是指向的
double 值。

请注意pa(它是数组名,表示地址)和&pa 之间的差别。正如您在本书前面看到的,在大多数情况下,
pa 都是数组第一个元素的地址,即&pa[0]。因此,它是单个指针的地址。但&pa 是整个数组(即三个指针
块)的地址。从数字上说,pa 和&pa 的值相同,但它们的类型不同。一种差别是,pa+1 为数组中下一个元
素的地址,而&pa+1 为数组pa 后面一个12 字节内存块的地址(这里假定地址为4 字节)。另一个差别是,
要得到第一个元素的值,只需对pa 解除一次引用,但需要对&pa 解除两次引用:

**&pa==*pa==pa[10]

程序清单7.19 使用了这里讨论的知识。出于演示的目的,函数f1()等都非常简单。正如注释指出的,
这个程序演示了auto 的C++98 替代品。

// 深入探讨函数指针.cpp : 函数指针的深入应用示例 // #include <iostream> #include <algorithm> // 函数声明 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 divide(int a, int b) { return b != 0 ? a / b : 0; } // 使用函数指针作为参数 void calculate(int a, int b, int (*operation)(int, int)) { std::cout << "结果: " << operation(a, b) << std::endl; } // 函数指针数组 int main() { // 1. 基本函数指针 int (*funcPtr)(int, int) = add; std::cout << "10 + 5 = " << funcPtr(10, 5) << std::endl; // 2. 函数指针数组 int (*operations[])(int, int) = {add, subtract, multiply, divide}; const char* opNames[] = {"加法", "减法", "乘法", "除法"}; for (int i = 0; i < 4; i++) { std::cout << opNames[i] << " (15, 3): "; calculate(15, 3, operations[i]); } // 3. 函数指针作为参数 std::cout << "\n使用函数指针作为参数:" << std::endl; calculate(20, 8, add); calculate(20, 8, subtract); // 4. typedef函数指针 typedef int (*MathFunc)(int, int); MathFunc func = multiply; std::cout << "使用typedef: 7 * 6 = " << func(7, 6) << std::endl; // 5. 使用using定义函数指针类型(C++11) using MathOp = int(*)(int, int); MathOp op = divide; std::cout << "使用using: 100 / 5 = " << op(100, 5) << std::endl; return 0; }

使用typedef 进行简化

除auto 外,C++还提供了其他简化声明的工具。您可能还记得,第5 章说过,关键字typedef 让您能够
创建类型别名:

typedef double real;

这里采用的方法是,将别名当做标识符进行声明,并在开头使用关键字typedef。因此,可将p_fun 声
明为程序清单7.19 使用的函数指针类型的别名:

typedef const double *(*p_fun)(const double *,int); p_fun p1=f1;

然后使用这个别名来简化代码:

p_fun pa[3]={f1,f2,f3}; p_fun (*pd)[3]=&pa;

使用typedef 可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。

总结

函数是C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功
能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使
得程序将参数传递给函数,并执行函数的代码。

在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函
数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。

C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始
地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是
等价的:

typeName arr[]; typeName *arr;

这两个声明都表明,arr 是指向typeName 的指针,但在编写函数代码时,可以像使用数组名那样使用
arr 来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const 指针,来保护原始数据的完整性。
由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。
另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,
就像STL 使用的算法一样。

C++提供了3 种表示C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char*
(char 指针),因此被作为char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串
函数检测空值字符来确定字符串的结尾。

C++还提供了string 类,用于表示字符串。函数可以接受string 对象作为参数以及将string 对象作为返
回值。string 类的方法size( )可用于判断其存储的字符串的长度。

C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。
然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适
用于类对象。

C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/10 7:04:51

Gyroflow完整教程:从陀螺仪数据到专业级视频稳定的实用指南

Gyroflow完整教程&#xff1a;从陀螺仪数据到专业级视频稳定的实用指南 【免费下载链接】gyroflow Video stabilization using gyroscope data 项目地址: https://gitcode.com/GitHub_Trending/gy/gyroflow Gyroflow是一款革命性的视频稳定工具&#xff0c;它通过解析设…

作者头像 李华
网站建设 2026/4/15 11:06:10

paper-reviewer:3步自动生成专业论文评审的终极解决方案

paper-reviewer&#xff1a;3步自动生成专业论文评审的终极解决方案 【免费下载链接】paper-reviewer Generate a comprehensive review from an arXiv paper, then turn it into a blog post. This project powers the website below for the HuggingFaces Daily Papers (http…

作者头像 李华
网站建设 2026/4/13 21:36:07

Gyroflow视频稳定全攻略:告别抖动困扰的专业解决方案

Gyroflow视频稳定全攻略&#xff1a;告别抖动困扰的专业解决方案 【免费下载链接】gyroflow Video stabilization using gyroscope data 项目地址: https://gitcode.com/GitHub_Trending/gy/gyroflow 还在为运动相机拍摄的抖动画面而烦恼吗&#xff1f;Gyroflow作为一款…

作者头像 李华
网站建设 2026/4/13 17:08:15

Pose-Search颠覆传统:用人体动作直接搜索图片的智能革命

Pose-Search颠覆传统&#xff1a;用人体动作直接搜索图片的智能革命 【免费下载链接】pose-search x6ud.github.io/pose-search 项目地址: https://gitcode.com/gh_mirrors/po/pose-search 在图片搜索领域&#xff0c;你是否曾为描述一个特定动作而绞尽脑汁&#xff1f;…

作者头像 李华
网站建设 2026/4/13 2:06:12

HTML可视化报告生成:基于TensorFlow-v2.9镜像输出实验结果

HTML可视化报告生成&#xff1a;基于TensorFlow-v2.9镜像输出实验结果 在深度学习项目日益复杂、团队协作频繁的今天&#xff0c;一个常见的痛点浮现出来&#xff1a;如何让一次模型训练的结果不仅“跑得通”&#xff0c;还能“讲得清”&#xff1f;我们常常看到研究员提交一堆…

作者头像 李华
网站建设 2026/4/9 21:11:28

使用git管理你的TensorFlow 2.9模型版本控制流程

使用 Git 管理你的 TensorFlow 2.9 模型版本控制流程 在深度学习项目中&#xff0c;你是否曾遇到过这样的场景&#xff1a;几个月前训练出一个效果不错的模型&#xff0c;但现在想复现结果时却发现——代码已经改过好几轮&#xff0c;配置文件散落在不同目录&#xff0c;权重文…

作者头像 李华