首页 > 编程笔记 > C++笔记 阅读:12

C++函数指针用法详解(附带实例)

函数指针C++ 中一种特殊的指针类型,它用于指向函数而不是普通数据。

顾名思义,函数指针存储了一个函数的内存地址,允许我们间接地调用函数。每个函数在 C++ 中都有自己的入口地址,函数指针就是通过这个地址来引用函数的。

函数指针的使用需要遵循类型安全的原则,即指针类型必须与所指向的函数的签名(返回类型和参数列表)完全匹配。这样,我们就可以通过函数指针来调用它所指向的函数,类似于通过普通指针访问它所指向的变量。

C++函数指针的基本用法

当我们定义普通指针来指向某个变量时,需要根据它所指向的这个变量的数据类型来确定指针的类型。

例如,我们的指针要指向一个 int 类型的变量,那么这个指针的类型就是 int*。同样地,如果我们需要定义函数指针来指向某个函数,同样需要根据它所指向的函数来确定这个函数指针的类型。

但与普通指针的类型只是在它所指向变量的类型后面加一个“*”不同,函数指针类型的确定要稍微复杂一些。在 C++ 中,我们定义一个函数指针的语法形式如下:
函数返回值类型 (*指针变量名)(形式参数列表);

概括起来,函数指针的定义与它所指函数的声明类似(返回值类型和形式参数列表相同),只不过是将函数名换成了“*”加上一个指针变量名。这也隐含一个信息,一个函数的函数名实际上就是指向该函数的指针。

例如,有这样一个函数:
// 一个普通的函数
void PrintPass( int nScore )
{
    cout<<nScore<<endl;
}

如果要定义一个函数指针来指向这个函数,可以使用如下代码:
// 定义函数指针
void (*pPrintFunc)( int nScore );
这样,pPrintFunc 就可以是指向 PrintPass() 函数的函数指针。当然,它也可以指向任何返回值类型为 void 且具有一个 int 类型参数的普通函数。

需要注意的是,当定义函数指针时,参数列表中的形式参数名是可选的。上述代码也可以简化为:
// 省略形式参数名的函数指针的定义
void (*pPrintFunc)( int );
当函数的形式参数较多时,通常省略形式参数名以简化函数指针的定义。

函数指针的定义比较烦琐,如果需要定义多个相同类型的函数指针,可以使用 typedef 关键字将这种函数指针类型定义为一种新的数据类型,然后用这种新的数据类型来定义函数指针。例如:
// 定义一种新的函数指针的数据类型
// 这种类型的函数指针可以指向的函数返回值类型是 void 的函数
// 这种函数有一个 int 类型的参数
typedef void (* PRINTFUNC )(int);
// 使用新的数据类型定义多个相同类型的函数指针
PRINTFUNC pFuncFailed;
PRINTFUNC pFuncPass;
这里定义了一种新的函数指针类型 PRINTFUNC,它表示这种类型的函数指针指向一个返回值类型为 void 且有一个 int 类型参数的函数。

完成函数指针的定义后,可以用函数名给函数指针赋值,让它指向这个函数:
// 用函数名给函数指针赋值
pPrintFunc = PrintPass;
虽然使用 typedef 关键字定义函数指针类型的方式,可以在一定程度上简化函数指针的定义,但我们仍然需要定义函数指针类型本身。在定义复杂函数指针类型时,这个过程不仅烦琐,而且很容易出错。

C++ 提供了 auto 关键字来解决这个问题。使用 auto 关键字作为函数指针的数据类型,可以直接定义一个函数指针并同时给它赋值以进行初始化。至于这种指针的具体类型定义,就留给编译器,编译器会根据这个指针的初始值自动推断它的具体数据类型。

利用 auto 关键字,可以这样定义函数指针:
// 利用auto关键字定义函数指针
// 编译器会在变量赋值的时候,自动推断函数指针的具体类型
auto pPrintFunc = PrintPass;

我们定义指向数据的普通数据指针,是为了通过它灵活地访问它所指向的数据。同样,定义指向函数的函数指针的目的,也是为了通过它灵活地访问它所指向的函数。与利用函数名直接调用函数相比,两者的效果完全相同,形式上也十分相近。例如:
// 通过函数名直接调用 PrintPass() 函数
PrintPass( 75 );

// 通过函数指针 pPrintFunc 调用它所指向的 PrintPass() 函数
(*pPrintFunc)( 75 );

// 更简化的形式
pPrintFunc( 75 );
这里,在函数指针前加上一个“*”符号,就相当于得到了它所指函数的函数名,类似于在普通数据指针前加上“*”可以得到它所指向的数据。然后,像普通函数调用一样,在函数名后的“()”中加上相应的参数,就可以实现对它所指向函数的调用。

更简单的方法是,把函数指针直接当作函数名,以普通函数调用的形式实现对它所指向函数的调用。由于函数指针 pPrintFunc 所指向的是 PrintPass() 函数,因此以上这三行代码是等效的,都是以 75 作为实际参数调用 PrintPass() 函数。

C++函数指针实现回调函数

读者可能会有疑问,既然使用函数指针调用函数与直接使用函数名调用函数没有什么差别,那么何必多此一举使用函数指针来调用函数呢?直接使用函数名调用函数不是更简单吗?

要回答这个问题,可以先简单回顾一下普通数据指针的意义:当我们需要在函数之间传递较大体积的数据时,可以通过传递指向这些数据的小体积的指针,从而间接地完成大体积数据的传递。由此可见,普通数据指针的意义在于传递数据。在一个程序中,除数据需要传递外,对数据的操作同样需要传递。而函数指针的意义就在于传递对数据的操作,即传递函数。

通常而言,一个函数定义完成之后,其行为就完全固定了。我们可以向函数传递数据参数来自定义函数处理的数据,但无法向函数传递另一个函数来对其行为进行自定义。而函数指针的意义就在于传递函数。

通过把函数指针作为参数传递,我们可以将指向某个函数的指针传递给另一个函数,而在这个函数中,我们可以通过传递进来的函数指针调用它所指向的函数,这个函数也被称为回调函数(callback function)。

根据传递进来的函数指针所指向函数的不同,不同的函数自然就表现出不同的行为,从而实现在函数之间的传递操作,达到通过参数对函数行为进行自定义的目的。

回调函数的使用正是为了让程序变得更加简单和灵活。回调函数可以把主调函数与被调函数分开,主调函数不必关心具体的被调函数是谁,只需知道存在一个具有特定原型和限制条件的被调函数。这就像在主调函数中留下了一个插口,规定了插口的规则,也就是函数的返回值和具体参数。而被调函数就是插头,可以插入任何符合这个插口规则的插头,从而实现主调函数的功能。

通过改变插入的插头,可以改变主调函数的功能,从而调整主调函数的实现。使用回调函数可以实现相同的算法框架来配合不同算法的实现,最终达到算法通用的目的。

下面我们来看一个实际的例子。在 STL 中,有一个 count_if() 算法,它通过提供一个函数指针类型的参数,使得外界可以向它传递表示统计规则的函数,从而让 count_if() 算法可以完成各种条件的统计。

在这里,我们利用函数指针实现一个简化版本的 mycount_if() 算法,让它能够通过函数指针参数接受多种统计规则函数,从而统计 vector 容器中符合各种条件的数据的个数:
// 定义函数指针类型 RuleFunc,它可以指向返回值为 bool 类型
// 同时拥有一个 int 类型参数的函数
typedef bool (*RuleFunc) (int);

// 定义算法框架函数
int mycount_if(const vector<int>& v, // 需要统计的容器
             RuleFunc is)     // 指向统计规则函数的函数指针
{
    int nTotal = 0;
    // 使用基于范围的 for 循环遍历容器中的数据
    for(int n:v)
    {
        // 通过函数指针调用规则函数
        // 判断当前数据是否符合统计规则
        if(is(n))
        {
            +++nTotal;    // 如果符合,则统计在内
        }
    }
    return nTotal;    // 返回统计结果
}

// 统计规则函数,判断分数是否及格
bool IsPass(int n)
{
    return n >= 60;
}

// 统计规则函数,判断分数是否优秀
bool IsGood(int n)
{
    return n >= 85;
}

// 利用 mycount_if() 算法统计 vector 容器中的数据
int main()
{
    // 待统计的容器,添加初始数据
    vector<int> vecScore = {54,87,65,31,94};
    // 使用 IsPass 函数名作为 mycount_if() 算法的参数
    // 统计容器中的及格分数的个数
    int nPass = mycount_if(vecScore,IsPass);
    // 更换统计规则函数,统计容器中的优秀分数的个数
    int nGood = mycount_if(vecScore,IsGood);

    // 输出结果
    cout<<"及格人数"<<nPass<<endl;
    cout<<"优秀人数"<<nGood<<endl;

    return 0;
}
在这里,mycount_if() 算法搭起的只是一个统计算法的框架:利用基于范围的 for 循环逐个遍历容器中的数据,然后调用函数指针所指向的规则函数,以判断当前数据是否符合统计规则。如果符合,则统计在内。具体的统计规则由通过函数指针参数传递的统计规则函数来定义。

虽然 mycount_if() 算法的框架是固定不变的,但因为传递给它的统计规则函数不同,所以能够统计出符合各种不同统计规则数据的个数。

这里的算法框架函数 mycount_if() 就好比一个插座,它通过函数指针参数提供了一个插口。而 IsPas() 和 IsGood() 算法规则函数就好比插头。只要插口和插头相互匹配(即函数的返回值和参数类型相同,算法框架函数的函数指针参数可以指向算法规则函数),我们就可以将插头插入插座(即以算法规则函数为参数调用算法框架函数),从而实现具体的功能。

不同的插头表现出不同的功能,如下图所示。这就像家中的同一个插座,插上电视机的插头可以看电视,而插上电饭锅的插头就可以做饭,体现出这种实现方式带来的算法通用性。


图 1 回调函数与插头理论

利用函数指针参数传递函数,然后在函数内部回调,可以改变一个函数的行为,对其行为进行参数化的自定义。正是因为这种灵活性,回调函数在 C++ 程序中被广泛应用,例如线程函数和通用算法中。

在实现某个算法时,我们往往只实现算法的基本框架,而算法的具体规则则交由回调函数来实现,也就是在算法中留下插口,留给算法的具体使用者来实现。这样,同一个算法可以适应不同的条件,从而达到通用的目的。

C++函数指针应用到STL算法中

就像前文介绍的 mycount_if() 算法一样,STL 中的大多数算法,特别是以 if 为后缀的算法,都可以通过给它们提供一个函数指针参数,来自定义其核心业务逻辑,最终使 STL 中的通用算法能够满足各种个性化的需求,实现真正的通用。

例如,我们可以为 STL 中的 count_if() 算法提供一个指向统计规则函数的函数指针,让它统计出容器中所有符合条件的数据的个数:
// 利用函数定义统计的规则,统计身高大于 170cm 的 Student 对象
bool countHeight( const Student& st )
{
    // 如果身高大于 170cm,则统计在内
    return st.GetHeight() > 170;
}

// 将统计规则函数的指针 countHeight 应用到 count_if() 算法中
// 这样 count_if() 算法将调用 countHeight() 函数来判断数据是否符合条件
int nCount = count_if(vecStu.begin(),
              vecStu.end(),
              countHeight ); // 统计规则
cout<<"身高大于 170cm 的学生有:"<<nCount<<endl;
在这段代码中,首先定义了一个函数 countHeight(),并在其中定义了自己的统计规则:判断 Student 对象的身高是否大于 170cm。如果大于,则返回 true,表示当前数据符合统计规则,并将它统计在内。

然后,将这个函数名,也就是指向这个函数的函数指针 countHeight 作为实际参数调用 STL 中的 count_if() 算法。当 count_if() 算法对容器中的数据进行统计时,会调用 countHeight() 函数来判断当前数据是否符合统计规则。如果符合,则统计在内。

换句话说,以函数指针的形式传递给 count_if() 算法的 countHeight() 函数成了一个回调函数,它会在 count_if() 算法中被调用。这样,可以在 countHeight() 函数中实现自己的统计规则,从而让 count_if() 算法按照自定义的规则进行统计,最终达到利用函数指针对 STL 算法进行自定义的目的。

从以上这段代码中,我们也注意到 countHeight() 函数中的身高标准已经固定,这使得算法失去了一定的灵活性。如果要统计大于另一个身高标准的人数,则不得不重新编写另一个统计规则函数。

为了让这个统计算法具有更大的灵活性,我们可以对统计规则函数 countHeight() 进行改写,将统计标准也作为函数的参数,在调用时再根据具体情况传入统计标准,使这个统计算法更加灵活:
// 将统计标准也作为参数,重新定义统计规则函数
bool countHeight( const int nHeight, const Student& st )
{
    // 如果身高大于标准身高,则统计在内
    return st.GetHeight() > nHeight;
}

在调用 count_if() 算法进行统计时,可以使用 STL 提供的 bind1st() 函数动态地绑定它的第一个参数,也就是指定统计的身高标准,从而灵活地完成各种身高人数的统计:
// 待统计的容器
vector<Student> vecStu;
// 用 push_back() 函数将 Student 对象添加到 vecStu 容器中

int nH = 0; // 身高标准
// 动态输入身高标准
cout<<"请输入身高标准"<<endl;
cin>>nH;

// 利用 countHeight() 函数进行统计
// 同时动态指定 nH 作为它的第一个参数,设定身高标准
int nCount = count_if(vecStu.begin(), vecStu.end(), // 统计范围
              bind1st( ptr_fun(countHeight),nH)); // 统计规则
cout<<"身高大于"<<nH<<"的人数是"<<nCount<<endl;
在这里,首先使用 ptr_fun() 函数将一个普通函数指针 countHeight 转换为一个函数对象,给函数换了一件“马甲”。

同样,这个函数对象与 countHeight() 函数一样,也需要两个参数,但是 count_if() 算法要求它的规则函数只能有一个参数。因此,我们得到函数对象后,用 bind1st() 函数将它的第一个参数 nHeight 绑定为用户输入的 nH,这样它就成了一个只拥有一个 Student& 类型参数的函数对象,从而可以直接用在 count_if() 算法中。

在 count_if() 算法执行时,它会逐个使用容器中的数据作为参数调用这个函数对象,实际上就是以 nH 和当前 Student 对象为参数来调用 countHeight() 函数,从而判断这个对象是否需要统计在内。这样,即使用户输入的身高标准不同,我们也可以用相同的 count_if() 算法调用形式来完成统计。

除可以在 STL 算法中使用普通的函数指针,将容器中的数据作为参数传递给它所指向的函数进行处理外,我们还可以在算法中使用指向类成员函数的指针,从而在算法处理过程中直接调用容器中数据的成员函数来实现功能。

例如,在前面的算法体育课中,为了让容器中的 Student 对象报数,我们先利用 for_each() 算法将 Student 对象传递给 CountOff() 函数,在这个函数中才间接地调用 Student 对象的 Report() 成员函数完成报数。报数的功能实现了,但过程有点烦琐,这显然不符合 STL 优雅的风格。

实际上,利用指向类成员函数的指针,这个过程可以直接一步完成:
// 使用类成员函数指针调用容器中数据的成员函数
for_each(vecStu.begin(), vecStu.end(),
    mem_fun_ref(&Student::Report)); // 调用 Report() 成员函数
在这里,首先使用“&”操作符来获得 Student 类的成员函数 Report() 的地址,即获得指向该成员函数的函数指针。因为 Report() 成员函数属于 Student 类,所以我们要在函数名之前加上类名,并使用“::”符号。

接着,使用 mem_fun_ref() 函数将这个成员函数指针构造成一个函数对象。虽然 Report() 成员函数表面上来看没有参数,但每个类成员函数都有一个隐藏的参数,即指向它自身的指针。经过 mem_fun_ref() 函数构造后,该函数对象可以接受一个 Student 类型的对象为参数,因此可以直接应用于 for_each() 算法。

在执行时,当 for_each() 算法将容器中的 Student 对象作为参数传递给这个函数对象时,实际上就是调用这个 Student 对象的 Report() 成员函数,从而完成报数的功能。

需要注意的是,如果容器中保存的是指向对象的指针,就应使用 mem_fun() 函数来完成这一任务。

将指向类成员函数的函数指针应用到 STL 算法中,使我们可以通过数据自身的函数来处理数据,省去了中间环节,因而这一方式被广泛地应用于 STL 算法。

相关文章