C++PrimerPlus 第七章 函数-C++的编程模块-7.10 函数指针

目录

7.10 函数指针

7.10.1 函数指针的基础知识

7.10.1.1 获取函数的地址

7.10.1.2 声明函数指针

7.10.1.3 使用指针来调用函数

7.10.2 函数指针示例

7.10.3 深入探讨函数指针

7.10.4 使用typedef进行简化


7.10 函数指针

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

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

7.10.1 函数指针的基础知识

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

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

7.10.1.1 获取函数的地址

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

        process(think); //passes address of think() to process()

        thought(think()); //passes return value of think() to thought()

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

7.10.1.2 声明函数指针

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

        double pam(int); //prototype

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

        double (*pf)(int);         //pf points to a function that takes

                                          //one int argument and that

                                          //returns type double

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

提示:

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

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

        double (*pf)(int); //pf points to a function that returns double

        double *pf(int); //pf() a function that returns a pointer-to-double

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

        double pam(int);

        double (*pf)(int);

        pf = pam; //pf mow points to the pam() function

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

        double ned(double);

        int ted(int);

        double (*pf)(int);

        pf = ned; //invalid -- mismatched signature

        pf = ted; //invalid -- mismatched return types

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

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

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

        estimate(50, pam); //function call telling estimate() to use pam()

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

7.10.1.3 使用指针来调用函数

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

        double pam(int);

        double (*pf)(int);

        pf = pam; //pf now points to the pam() function

        double x = pam(4); //call pam() using the function name

        double y = (*pf)(5); //call pam() using the pointer pf

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

        double y = pf(5); //also call pam() using the pointer pf

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

历史与逻辑

真是非常棒的语法!为何pf和(*pf)等价呢?一种学派认为,由于pf是函数指针,而*pf是函数,因此应将(*pf)()用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf()用作函数调用使用。C++进行了折衷——这2种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是相互冲突的。在认为这种折衷粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正是人类思维活动的特点。

7.10.2 函数指针示例

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

程序清单7.18 fun_ptr.cpp

//fun_ptr.cpp -- pointers to functions
#include<iostream>
double betsy(int);
double pam(int);

//second argument is pointer to type double function that
//takes a type int argument
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";
}

下面是运行该程序的情况:

        How many lines of code do you need? 30

        Here's Betsy's estimate:

        30 lines will take 1.5 hour(s)

        Here's Pam's estimate:

        30 lines will take 1.26 hour(s)

下面是再次运行该程序的情况:

        How many lines of code do you need? 100

        Here's Betsy's estimate:

        100 lines will take 5 hour(s)

        Here's Pam's estimate:

        100 lines will take 7 hour(s)

7.10.3 深入探讨函数指针

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

        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; //C++11 automatic type deduction

现在来看下面的语句:

        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; //C++11 automatic type deduction

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

        *pd[3]; //an array of 3 pointers

        (*pd)[3]; //a pointer to an array of 3 elements

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

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

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

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

程序清单7.19 arfupt.cpp

//arfupt.cpp -- an array of function pointers
#include<iostream>
//various notations, same signatures
const double* f1(const double ar[], int n);
const double* f2(const double[], int);
const double* f3(const double*, int);

int main()
{
	using namespace std;
	double av[3] = { 1112.3,1542.6,2227.9 };

	//pointer to a function
	const double* (*p1)(const double*, int) = f1;
	auto p2 = f2;	//C++11 automatic type deduction
	//pre-C++11 can use the following code instead
	//const double*(*p2)(const double*, int) = f2
	cout << "Using pointers to functions:\n";
	cout << " Address Value\n";
	cout << (*p1)(av, 3) << ": " << *(*p1)(av, 3) << endl;
	cout << p2(av, 3) << ": " << *p2(av, 3) << endl;

	//pa an array of pointers
	//auto doesn't work with list initialization
	const double* (*pa[3])(const double*, int) = { f1,f2,f3 };
	//but it does work for initializing to a single value
	//pb a pointer to first element of pa
	auto pb = pa;
	//pre-C++11 can use the following code instead
	//const double*(**pb)(const double*, int) = pa;
	cout << "\nUsing an array of pointers to functions:\n";
	cout << " Address Value\n";
	for (int i = 0; i < 3; i++)
		cout << pa[i](av, 3) << ": " << *pa[i](av, 3) << endl;
	cout << "\nUsing a pointer to a pointer to a function:\n";
	cout << " Address Value\n";
	for (int i = 0; i < 3; i++)
		cout << pb[i](av, 3) << ": " << *pb[i](av, 3) << endl;

	//what about a pointer to an array of function pointers
	cout << "\nUsing pointer to an array of pointers:\n";
	cout << " Address Value\n";
	//easy way to declare pc
	auto pc = &pa;
	//pre-C++11 can use the following code instead
	//const double *(*(*pc)[3])(const double *, int) = &pa;
	cout << (*pc)[0](av, 3) << ": " << *(*pc)[0](av, 3) << endl;
	//hard way to declare pd
	const double* (*(*pd)[3])(const double*, int) = &pa;
	//store return value in pdb
	const double* pdb = (*pd)[1](av, 3);
	cout << pdb << ": " << *pdb << endl;
	//alternative notation
	cout << (*(*pd)[2])(av, 3) << ": " << *(*(*pd)[2])(av, 3) << endl;
	//cin.get();
	return 0;
}

//some rather dull functions

const double* f1(const double* ar, int n)
{
	return ar;
}
const double* f2(const double ar[], int n)
{
	return ar + 1;
}
const double* f3(const double ar[], int n)
{
	return ar + 2;
}

该程序的输出如下:

        Using pointers to functions:

         Address Value

        004FF9E0: 1112.3

        004FF9E8: 1542.6

        Using an array of pointers to functions:

         Address Value

        004FF9E0: 1112.3

        004FF9E8: 1542.6

        004FF9F0: 2227.9

        Using a pointer to a pointer to a function:

         Address Value

        004FF9E0: 1112.3

        004FF9E8: 1542.6

        004FF9F0: 2227.9

        Using pointer to an array of pointers:

         Address Value

        004FF9E0: 1112.3

        004FF9E8: 1542.6

        004FF9F0: 2227.9

显示的地址为数组av中double值的存储位置。

这个示例可能看起来比较深奥,但指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术(参见第13章)。所幸的是,这些细节由编译器处理。

感谢auto

C++11的目标之一是让C++更容易使用,从而让程序员将主要精力放在设计而不是细节上。程序清单7.19演示了这一点:

        auto pc = &pa; //C++11 automatic type deduction

        const double*(*(*pd)[3])(const double *, int) = &pa; //C++98, do it yourself

自动类型推断功能表明,编译器的角色发生了改变。在C++98中,编译器利用其知识帮助您发现错误,而在C++11中,编译器利用其知识帮助您进行正确的声明。

存在一个潜在的缺点。自动类型推断确保变量的类型与赋给它的初值的类型一致,但您提供的初值的类型可能不对:

        auto pc = *pa; //oops! used *pa instead of &pa

上述声明导致pc的类型与*pa一致,在程序清单7.19中,后面使用它时假定其类型与&pa相同,这将导致编译错误。

 

7.10.4 使用typedef进行简化

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

        typedef double real; //makes real another name for double

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

        typedef const double*(*p_fun)(const double *, int); //p_fun now a type name

        p_fun p1 = f1; //p1 points to the f1() function

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

        p_fun pa[3] = {f1, f2, f3}; //pa an array of 3 function pointers

        p_fun (*pa)[3] = &pa; //pd points to an array of 3 function pointers

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hank_W

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值