C++那些细节--函数指针

一.简介

C或者C++中最灵活的东东就是指针了,在操作一个对象,或者数组等等,我们常常用到指针,可以给编程带来很多灵活性。但是,指针不仅仅能指向固定数据类型或是对象,指针还可以指向函数,这就是所谓的函数指针。
有了函数指针,我们可以通过指针调用函数,更重要的是我们可以将函数指针作为参数传递给函数,进而可以进行完成注册,回调等等功能,可以说,有了函数指针,我们的程序可以设计的更加灵活了。

二.普通函数指针

1.函数指针的定义方式

看一个最简单的例子:
// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//定义一个函数指针
void (*pFunc)(int);

//声明并定义一个函数
void PrintInt(int i)
{
	cout<<i<<endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	//将函数赋给函数指针
	//不使用&符号也可以,因为函数名称本来就是函数的首地址,类似数组
	pFunc = &PrintInt;
	
	//通过函数指针来调用函数
	pFunc(1);
	//调用时也可以写的完整规范一些(*pFunc)(1)符合一般指针的标准

	system("pause");
	return 0;
}
结果:
1
请按任意键继续. . .

声明函数指针的时候,一般的格式为 返回值 (*函数指针名)(参数1,参数2...)但是这样声明有一个弊端,就是麻烦,每次需要一个函数指针的时候,都需要写这么一大串,很麻烦,所以我们就想到了用typedef这个关键字,定义某一种类型的函数指针,给它一个别名,这样,每次使用这种函数指针的时候就可以像定义一个int型变量那么简单。

比如上面的函数指针,我们就可以这样写:
// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//使用typedef定义函数指针,此时,pFunc就不再是函数指针本身的名称,而是这类函数指针的一个别名
typedef void (*pFunc)(int);

//声明并定义一个函数
void PrintInt(int i)
{
	cout<<i<<endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	//定义一个函数指针,并赋予其初始值
	pFunc func = PrintInt;
	
	//通过函数指针来调用函数
	func(1);
	

	system("pause");
	return 0;
}
结果:
1
请按任意键继续. . .

这样写之后,我们就定义了一种函数指针的类型,返回值为void,参数为int的函数指针,之后使用pFunc就可以很方便的定义这种类型的函数指针啦!



三.成员函数指针

成员函数指针是C++中最麻烦的东东之一,准确的说是非静态成员函数指针。

1.静态成员函数指针

静态成员函数指针与普通函数指针一样,我们看一个例子:

// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//Base类
class Base
{
public:
	static void Print(int num)
	{
		cout<<num<<endl;
	}
};

//定义一个静态成员函数指针
typedef void (*pFunc)(int);


int _tmain(int argc, _TCHAR* argv[])
{
	//将静态成员函数赋给函数指针
	pFunc func = Base::Print;

	//通过函数指针调用函数
	func(1);

	system("pause");
	return 0;
}

结果:

1
请按任意键继续. . .

静态成员函数与普通的函数没有太多区别,虽然他们定义在类中,但是这个成员函数不会因为对象的不同而做出不同的处理,因为它没有this指针,所以我们可以将它看成普通的函数。


2.非静态成员函数


但是非静态成员函指针就麻烦得多,原因非静态成员函数有一个隐藏的参数--this指针,这个东东在不同的对象中是不一样的,所以很麻烦。我们定义非静态成员函数指针的时候就需要将对象的类型也写出来。调用的时候,也要根据相应的对象来调用这个函数。看一个例子:

// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//Base类
class Base
{
public:
	void Print(int num)
	{
		cout<<num<<endl;
	}
};

//定义一个成员函数指针
typedef void (Base::*pFunc)(int);


int _tmain(int argc, _TCHAR* argv[])
{
	Base base;
	//取成员函数的地址,必须以这种形式,不能以对象的形式获取,而且必须加上&符号
	pFunc func = &Base::Print;
	//调用的时候写法也比较特殊,base.*func表示函数体,是一个整体,需要用()
	(base.*func)(1);
	
	system("pause");
	return 0;
}
结果:

1
请按任意键继续. . .

分析一下成员函数指针的定义和使用,我们这样定义函数指针,(Base::*pFunc)(int),其实就相当于(*pFunc)(Base*, int),相当于普通的函数指针需要多一个this指针作为参数,而这个this指针在不同的对象中一定是不同的,所以成员函数指针之间是不能互相转化的,只有同类型的对象的函数才能赋给这种对象的函数指针。

在指针赋值的时候,注意一下写法,普通的函数指针在赋值的时候,可以不写&符号,但是成员函数指针赋值的时候比较严格,如果不写的话会报出这样的错误:

error C3867: “Base::Print”: 函数调用缺少参数列表;请使用“&Base::Print”创建指向成员的指针

而且在赋值的时候,不要用对象赋值,要用 类名::函数名 这种方式赋值,使用 对象名.函数名的时候会报出这样的错误:

 error C2276: “&”: 绑定成员函数表达式上的非法操作

可见,对于成员函数指针的写法还是挺严苛的。

最后再分析一下使用,由于成员函数指针需要一个this指针作为参数,这个参数又不能直接给出,所以我们就只能通过对象来调用函数,在使用函数的时候,由于函数是一个整体,所以需要用(),在括号内部,我们通过*func获得函数,然后前面使用base.就将base作为this指针传递给了函数。

那么,既然这个函数是非静态成员函数,那么这个函数支不支持动态绑定呢?换句话说就是,我们声明的函数指针是基类的函数指针,子类覆写了这个函数,那么,用子类对象调用的时候,是调用基类的函数还是子类的函数呢?我们看一个例子:

// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//Base类
class Base
{
public:
	//虚函数
	virtual void Print(int num)
	{
		cout<<"base: "<<num<<endl;
	}
};

class Child : public Base
{
public:
	//覆写基类的Print方法
	void Print(int num) override
	{
		cout<<"child: "<<num<<endl;
	}
};


//定义一个成员函数指针
typedef void (Base::*pFunc)(int);

int _tmain(int argc, _TCHAR* argv[])
{
	Base base;
	Child child;
	//给函数指针赋值的时候,必须是用声明函数指针的类来赋值(此处为Base)
	pFunc func = &(Base::Print);
	//通过base对象调用函数指针
	(base.*func)(1);
	//通过child对象调用函数指针
	(child.*func)(1);
	//查看函数指针的大小
	cout<<"sizeof Member Function: "<<sizeof(func)<<endl;

	system("pause");
	return 0;
}
结果:

base: 1
child: 1
sizeof Member Function: 4
请按任意键继续. . .


从上面的结果,我们看出,在最后使用函数指针的时候,这个this指针的对象可以是我们声明函数指针的时候的对象的子类,并且如果我们覆写了基类的函数,是可以调用子类的函数的(注意是覆写基类的virtual函数,如果只是单纯的覆盖是没有多态效果的)。

我们分析一下原因,还是这样看待这个函数指针,把它看成普通函数指针增加了一个类对象的this指针作为参数,这个形参我们可以声明为基类的指针,我们给实参的时候,可以给基类的对象,当然也可以给子类对象,这就跟我们普通函数传递参数的情况一样,然后这个参数传递进去,如果有virtual函数,那么就可以触发多态。但是我们在给函数指针赋值的时候,却只能使用基类的函数,因为我们没有声明子类的函数。


四.函数指针的作用

1.函数指针作为函数的参数

有了函数指针,我们就可以像传递普通指针那样将一个函数作为参数传递给另一个函数,这大大的增加了我们编程的灵活性。比如处理一个事务,我们可以随意更换处理的handler,当然,使用面向对象思想,通过多态也可以实现相应的功能。我们看一个例子:
// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//定义pcall这种函数指针的类型
typedef int (*pcall)(int, int);

//处理函数,接受一个pcall类型的函数作为参数
void Func(pcall p, int x, int y)
{
	cout<<"begin func:"<<endl;
	cout<<"result is : "<<p(x, y)<<endl;
	cout<<"end func"<<endl;
}

//加法函数
int AddFunc(int x, int y)
{
	return x + y;
}

//减法函数
int SubFunc(int x, int y)
{
	return x - y;
}

int _tmain(int argc, _TCHAR* argv[])
{
	//将函数指针作为参数传递给函数
	//&符号可以省略
	Func(&AddFunc, 2, 1);
	Func(&SubFunc, 2, 1);

	system("pause");
	return 0;
}
结果:
begin func:
result is : 3
end func
begin func:
result is : 1
end func
请按任意键继续. . .


2.函数回调

能将函数作为参数进行传递并不是我们最终的目的,函数指针最有用的功能之一就是回调。关于函数普通调用和回调,我们用买东西做个比喻,普通调用就好比我们直接去买东西,到了商店就买到了东西;而函数回调就好比我们去蛋糕店预定一个蛋糕,这时蛋糕店肯定会留下你的联系方式,当蛋糕做好了,蛋糕店就会给你打电话,让你去取蛋糕。从这里,我们看到,函数回调也是需要有一些前提步骤的,首先我们要将回调的函数注册给调用方,这就好比我们去蛋糕店留下了联系方式,而当某种条件满足了的时候,就调用回调函数,这个条件在我们的例子中就是蛋糕做好了。最后,蛋糕店给你打电话让你去取蛋糕,这个过程就是调用我们注册好的函数了。
回调函数有什么好处呢?还是拿买蛋糕的例子来说,如果不用回调函数,那么,我们如果去蛋糕店买蛋糕,买完之后,我们并不知道蛋糕什么时候做好,那么要么我们就得一直呆在蛋糕店等着,要么我们就得隔一会儿给蛋糕店打一个电话,问一下蛋糕有没有做好。看一个没有用回调函数的例子:
// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

class Baker
{
private:
	int m_iTime;			  //做蛋糕的时间
	static const int m_iMaxTime = 10;//假设10分钟做完
public:
	//做蛋糕,如果做好了返回true,否则返回false
	bool MakeCake();
	//构造函数
	Baker();
};

Baker::Baker() : m_iTime(0){}

bool Baker::MakeCake()
{
	//假设每次调用该函数,m_iTime+1
	m_iTime += 1;
	if (m_iTime == m_iMaxTime)
	{
		cout<<"蛋糕做好了!"<<endl;
		return true;
	}
	return false;
}



//客户函数
void GetMyCake()
{
	cout<<"我来取蛋糕啦!"<<endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	//创建一个蛋糕店
	Baker baker;
	//做蛋糕
	while(1)
	{
		//客户如果想第一时间知道蛋糕做没做好,就必须每时每刻查看蛋糕的情况
		if (baker.MakeCake())
		{
			GetMyCake();
			break;
		}
		else
			cout<<"蛋糕没做好"<<endl;
	}

	system("pause");
	return 0;
}
结果:
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕没做好
蛋糕做好了!
我来取蛋糕啦!
请按任意键继续. .

这里,我们如果想要第一时间知道蛋糕有没有做好,就必须一致查询蛋糕做没做好,换句话说,我们不能去干别的事情去了,要一直在那里等蛋糕。在程序中来看,我们的客户不停的进行查询,如果这是一个线程的话,那么这个线程在这段时间就不能继续做其他事了。 .
而如果我们用回调函数,这个过程就变成了这样:
// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
using namespace std;

//声明一种回调函数
typedef void (*CallBackFunc)(void);

class Baker
{
private:
	int m_iTime;			  //做蛋糕的时间
	static const int m_iMaxTime = 10;//假设10分钟做完
	CallBackFunc m_pfCallBack;//回调函数
public:
	//注册:留下买蛋糕的人的联系方式
	void Invoke(CallBackFunc);
	//打电话通知买蛋糕的人
	void Notify();
	//做蛋糕,如果做好了,直接通知客户
	void MakeCake();
	//构造函数
	Baker();
};

Baker::Baker() : m_iTime(0){}

void Baker::MakeCake()
{
	while(m_iTime < m_iMaxTime)
	{
		//假设每次调用该函数,m_iTime+1
		m_iTime += 1;
	}
	cout<<"蛋糕做好了!"<<endl;
	Notify();
}

void Baker::Invoke(CallBackFunc pfunc)
{
	//注册过程
	m_pfCallBack = pfunc;
	cout<<"留下了您的联系方式!"<<endl;
}

void Baker::Notify()
{
	if (m_pfCallBack)
		m_pfCallBack();
}


//客户函数
void GetMyCake()
{
	cout<<"我来取蛋糕啦!"<<endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	//创建一个蛋糕店
	Baker baker;
	//注册:留下联系方式
	baker.Invoke(GetMyCake);
	//注册之后,客户就不需要实时查询了
	cout<<"好了叫我就好,我去玩儿啦!"<<endl;
	//做蛋糕
	baker.MakeCake();

	system("pause");
	return 0;
}
结果:
留下了您的联系方式!
好了叫我就好,我去玩儿啦!
蛋糕做好了!
我来取蛋糕啦!
请按任意键继续. . .

通过函数回调,虽然实现过程比较麻烦,先得注册,然后再类对象内部进行判断。但是这样做是有好处的,就是客户在注册之后,完全不需要再管蛋糕做没做好了,只要蛋糕做好了,蛋糕店会立刻通知客户。比如我们做异步加载时,如果我们需要资源,就可以主线程去加载线程注册一下,然后主线程继续做其他工作,加载线程加载完所需要的东西之后,反过来通过回调函数通知主线程,这样,加载和主要功能就可以同时进行,大大的提升了用户体验。

其实函数指针回调在C#中的实现就是代理,C#将回调这件事做得特别简单,可惜C++中没有代理,所以我们做起来还是比较麻烦的,不过回调真的灰常有用。


五.其他问题

函数指针和指针函数

函数指针和指针函数仅仅是名字比较像,写法容易搞混,其他并没有什么联系,二者可以共存。
函数指针我们说过了,就是指向函数的指针,其本质是一个指针。
指针函数,指的是函数的返回值是一个指针,其本质是一个函数。
两者容易搞混的地方就在于写法:
//定义一个函数指针
typedef int (*pFunc)(void);
//定义一个指针函数
int* PointerFunc(void)
{
	int a = 1;
	return &a;
}
函数指针*和名字是放在一起的,而指针函数*是和返回值放在一起的。

当然,我们也可以定义一个指针函数的函数指针:
//定义一个指针函数的函数指针
typedef int* (*pFUN)(void);






参考链接:http://blog.chinaunix.net/uid-10386087-id-2959230.html

  • 0
    点赞
  • 3
    收藏
  • 打赏
    打赏
  • 3
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 3

打赏作者

puppet_master

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值