c++ 指针

原文地址
摘要:这篇文章详细介绍C/C++的函数指针,请先看以下几个主题:使用函数指针定义新的类型、使用函数指针作为参数、使用函数指针作为返回值、使用函数指针作为回调函数、使用函数指针数组,使用类的静态函数成员的函数指针、使用类的普通函数成员的指针、定义函数指针数组类型、使用函数指针实现后绑定以及在结构体中定义函数指针。如果您对以上这几个主题都很了解,那么恭喜您,这篇文章不适合您啦~。在一些开源软件中,如Boost, Qt, lam-mpi中我们经常看到函数指针,本文目的是彻底搞定函数指针的语法和语义,至于怎样将函数指针应用到系统架构中不在此文的讨论范围中。各位看官,有砖拍砖啊~

  1. 无处不见的函数指针
    
    使用函数指针可以设计出更优雅的程序,比如设计一个集群的通信框架的底层通信系统:首先将要每个消息的对应处理函数的指针保存映射表中(使用STL的map,键是消息的标志,值是对应的函数指针),然后启动一个线程在结点上的某个端口侦听,收到消息后,根据消息的编号,从映射表中找到对应的函数入口,将消息体数据作为参数传给相应的函数。我曾看过lam-mpi在启动集群中每个结点的进程时的实现,该模块的最上层就是一个结构体,这个结构体中仅是由函数指针构成,每个函数指针都指向一个子模块,这样做的好处就是在运行时期间可以自由的切换子模块。比如某个子模块不适合某个体系结构,只需要改动函数指针,指向另外一个模块就可。
    

在平时的程序设计中,经常遇到函数指针。如EnumWindows这个函数的参数,C语言库函数qsort的参数,定义新的线程时,这些地方函数指针都是作为回调函数来应用的。

还有就是unix的库函数signal(sys/signal.h)(这个函数我们将多次用到)的声明形式为:

void (signal)(int signo,void (func)(int)))(int);

这个形式是相当复杂的,因为它不仅使用函数指针作为参数,而且返回类型还是函数指针(虽然这个函数在POSIX中不被推荐使用了)。

还有些底层实现实际上也用到了函数指针,可能你已经猜到了。嗯,就是C++中的多态。这是一个典型的迟绑定(late-binding)的例子,因为在编译时是无法确定到底绑定到哪个函数上执行,只有在运行时的时候才能确定。这个可以通过下面这个例子来帮助理解:

Shape *pSh;

scanf(“%d”,&choice);

if(choice)

{

pSh= new Rectangle();

}

else

{

pSh= new Square();}

pSh->display();

对于上面这段代码,做以下几个假设:

(1) Square继承自Rectange

(2) Rectangle继承自Shape

(3) display为虚函数,在每个Shape的子类链中都必须实现

正是因为在编译期间无法确定choice的值,所以在编译到最后一行的时候无法确定应该绑定到那个一个函数上,只能在运行期间根据choice的值,来确定要绑定的函数的地址。

总之,使用指针可以让我们写出更加优雅,高效,灵活的程序。另外,和普通指针相比,函数指针还有一个好处就是你不用担心内存释放问题。

但是,函数指针确实很难学的,我认为难学的东西主要有两个原因:(1)语法过于复杂。(2)语义过于复杂。从哲学上讲,可以对应为(1)形式过于复杂。(2)内容过于复杂。

比如,如果我们要描述“美女”这种动物(老婆不要生气啊~),如果在原始时代,我们可能需要通过以下这种方式:

 _____                 &&&&_) )

/,---< &&&&&&\ \

( )cc@@ )- - &&\ \

C   >/                \<   |&/

 \_O/ - 哇塞          _`*-'_/ /

,- >o<-. / ____ _/

/ / \ / /\ ))

/ /| | |\ \ / / ) |

\ | | |/ / \ \ / |

_\ | |_/ \ _ |

//`|\ //____|

|  | |                  \  \|

|  | |                   `. )

|  | |                   / /

|__|_|_                 /_/|

(____)_)                |\_\_

而现在我们只需要用语言来抽象就行,即用两个汉字“美女”或者英文“beauty”就行了。这就是形式上的简化,也就方便了我们的交流。另外一种就是内容上的复杂度过高,一个高度抽象的表达式后面蕴含着巨大的复杂度对于我们理解问题也是很难的,例如:

P=NP?

由于接触过的书上所讲的关于函数指针方面的都是蜻蜓点水一样,让我很不满足。我认为C/C++语言函数指针难学的主要原因是由于其形式上的定义过于复杂,但是在内容上我们一定要搞清楚函数的本质。函数的本质就是表达式的抽象,它在内存中对应的数据结构为堆栈帧,它表示一段连续指令序列,这段连续指令序列在内存中有一个确定的起始地址,它执行时一般需要传入参数,执行结束后会返回一个参数。和函数相关的,应该大致就是这些内容吧。

2 函数指针简单介绍

2.1 什么是函数指针

函数指针是一个指向函数的指针(呃,貌似是废话),函数指针表示一个函数的入口地址。使用函数指针的好处就是在处理“在运行时根据数据的具体状态来选择相应的处理方式”这种需求时更加灵活。

2.2 一个简单的例子

下面是一个简单的使用函数指针取代switch-case语句的例子,为了能够比较出二者效率差异,所以在循环中进行了大量的计算。

/*

*Author:Choas Lee

*Date:2012-02-28

*/

include<stdio.h>

define UNIXEVN

if defined(UNIXENV)

include<sys/time.h>

endif

define N 1000000

define COE 1000000

float add(float a,float b){return a+b;}

float minus(float a,float b){return a-b;}

float multiply(float a,float b){return a*b;}

float divide(float a,float b){return a/b;}

typedef float (*pf)(float,float);

void switch_impl(float a,float b,char op)

{

  float result=0.0; 

  switch(op) 

  { 

         case '+': 

                result=add(a,b); 

                break; 

         case '-': 

                result=minus(a,b); 

                break; 

         case '*': 

                result=multiply(a,b); 

                break; 

         case '/': 

                result=divide(a,b); 

                break; 

  } 

}

void switch_fp_impl(float a,float b,pf p)

{

  float result=0.0; 

  result=p(a,b); 

}

int conversion(struct timeval tmp_time)

{

  return tmp_time.tv_sec*COE+tmp_time.tv_usec; 

}

int main()

{

  int i=0; 

if defined(UNIXENV)

  struct timeval start_point,end_point; 



  gettimeofday(&start_point,NULL); 

endif

  for(i=0;i<N;i++) 

  { 

         switch_impl(12.32,54.14,'-'); 

  } 

if defined(UNIXENV)

  gettimeofday(&end_point,NULL); 

  printf("check point 1:%d\n",conversion(end_point)-conversion(start_point)); 

  

  gettimeofday(&start_point,NULL); 

endif

  for(i=0;i<N;i++) 

  { 

         switch_fp_impl(12.32,54.14,minus); 

  } 

if defined(UNIXENV)

  gettimeofday(&end_point,NULL); 

  printf("check point 2:%d\n",conversion(end_point)-conversion(start_point)); 

endif

  return 0; 

}

下面是执行结果:

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:22588

check point 2:19407

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:22656

check point 2:19399

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:22559

check point 2:19380

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:22181

check point 2:19667

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:22226

check point 2:19813

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:22141

check point 2:19893

[lichao@sg01 replaceswitch]$ ./replaceswitch

check point 1:21640

check point 2:19745

从上面可以看出,使用函数指针:(一)在某种程度上简化程序的设计(二)可以提高效率。在这个例子中,使用函数指针可以提高10%的效率。

注意:以上代码在unix环境下实现的,如果要在windows下运行,可以稍微改下,把“#define UNIXENV”行删掉即可

3 C/C++函数指针的语法

从语法上讲,有两种不兼容的函数指针形式:

(1) 指向C语言函数和C++静态成员函数的函数指针

(2) 指向C++非静态成员函数的函数指针

不兼容的原因是因为在使用C++非静态成员函数的函数指针时,需要一个指向类的实例的this指针,而前一类不需要。

3.1 定义一个函数指针

指针是变量,所以函数指针也是变量,因此可以使用变量定义的方式来定义函数指针,对于普通的指针,可以这么定义:

int a=10;

int *pa=&a;

这里,pa是一个指向整型的指针,定义这个指针的形式为:

int * pa;

区别于定义非指针的普通变量的“形式”就是在类型中间和指针名称中间加了一个“*”,所以能够表达不同的“内容”。这种形式对于表达的内容是完备的,因为它说明了两点:(1)这是一个指针(2)这是一个指向整型变量的指针

以下给出三个函数指针定义的形式,第一个是C语言的函数指针,第二个和第三个是C++的函数指针的定义形式(都是指向非静态函数成员的函数指针):

int (*pFunction)(float,char,char)=NULL;

int (MyClass::*pMemberFunction)(float,char,char)=NULL;

int (MyClass::*pConstMemberFunction)(float,char,char) const=NULL;

我们先不管函数指针的定义形式,如果让我们自己来设计指向函数的函数指针的定义形式的话,我们会怎么设计?

首先,要记住一点的就是形式一定要具备完备性,能表达出我们所要表达的内容,即指向函数这个事实。我们知道普通变量指针可以指向对应类型的任何变量,同样函数指针也应该能够指向对应类型的任何变量。对应的函数类型靠什么来确定?这个我们可以想一下C++的函数重载靠什么来区分不同的函数?这里,函数类型是靠这几个方面来确定的:(1)函数的参数个数(2)函数的参数类型(3)函数的返回值类型。所以我们要设计一种形式,这种形式定义的函数指针能够准确的指向这种函数类型的任何函数。

在C语言中这种形式为:

返回类型 (*函数指针名称)(参数类型,参数类型,参数类型,…);

嗯,定义变量的形式显然不是我们通常见到的这种形式:

类型名称 变量名称;

但是,这也是为了表达函数这种相对复杂的语义而不得已采用的非一致表示形式的方法。因为定义的这个函数指针变量,能够明确的表达出它指向什么类型的函数,这个函数都有哪些类型的参数这些信息,确切的说,它是完备的。你可能会问为什么要加括号?形式上讲能不能更简洁点?不能,因为不加括号就会产生二义性:

返回类型 *函数指针名称(参数类型,参数类型,参数类型,…);

这样的定义形式定义了一个“返回类型为‘返回类型*’参数为(参数类型,参数类型,参数类型,…)的函数而不是函数指针了。

接下来,对于C++来说,下面这样的定义形式也就不难理解了(加上类名称是为了区分不同类中定义的相同名称的成员函数):

返回类型 (类名称::*函数成员名称)(参数类型,参数类型,参数类型,….)

3.2 函数的调用规则

一般来说,不用太关注这个问题。调用规则主要是指函数被调用的方式,常见的有_stdcall,_fastcall,_pascal,_cdecl等规则。不同的规则在参数压入堆栈的顺序是不同的,同时在有调用者清理压入堆栈的参数还是由被调用者清理压入堆栈的参数上也是不同的。一般来说,如果你没有显式的说明调用规则的话,编译器会统一按照_cdecl来处理。

3.3 给函数指针赋值和调用

给函数指针赋值,就是为函数指针指定一个函数名称。这个过程很简单,下面是两个例子:

int func1(float f,int a,int b){return f*a/b;}

int func2(float f,int a,int b){return fab}

然后我们给函数指针pFunction赋值:

pFunction=func1;

pFunction=&func2;

上面这段代码说明了两个问题:(1)一个函数指针可以多次赋值(想想C++中的引用)(2)取地址符号是可选的,却是推荐使用的。

我们可以思考一下为什么取地址符号是可选的,在普通的指针变量赋值时,如上面所示,需要加取地址符号,而这里却是可选的?这是由于要同时考虑到两个因素(1)避免二义性(2)形式一致性。在普通指针赋值,需要加取地址符号是为了区别于将地址还是将内容赋给指针。而在函数赋值时没有这种考虑,因为这里的语义是清晰的,加上&符号是为了和普通指针变量一致---“因为一致的时候就不容易出错”。

最后我们来使用这个函数

pFunction(10.0,’a’,’b’);

(*pFunction)(10.0,’a’,’b’);

   上面这两种使用函数指针调用函数的方式都是可以的,原因和上面一样。

下面来说明C++中的函数指针赋值和调用,这里说明非静态函数成员的情况,C++中规则要求的严格的多了。让我感觉C++就像函数指针的后爸一样,对函数指针要求特别死,或许是因为他有一个函数对象这个亲儿子。

在C++中,对于赋值,你必须要加“&”,而且你还必须再次之前已经定义好了一个类实例,取地址符号要操作于这个类实例的对应的函数成员上。在使用成员函数的指针调用成员函数时,你必须要加类实例的名称,然后再使用.或者->来使用成员函数指针。举例如下:

MyClass

{

public:

  int func1(float f,char a,char b)

  {

         return f*a*b;

  }

  int func2(float f,char a,char b) const

  {

return f*a/b; }

}

首先来赋值:

MyClass mc;

pMemberFunction= &mc.func1; //必须要加取地址符号

pConstMemberFunction = &mc.func2;

接下来,调用函数:

(mc.*pMemberFunction)(10.0,’a’,’b’);

(mc.*pConstMemberFunction)(10.0,’a’,’b’);

我感觉,C++简直在虐待函数指针啊。

下面是一个完整的例子:

/*

*Author:Choas Lee

*Date:2012-02-28

*/

include<stdio.h>

float func1(float f,char a,char b)

{

  printf("func1\n"); 

  return f*a/b; 

}

float func2(float f,char a,char b)

{

  printf("func2\n"); 

  return f*a*b; 

}

class MyClass

{

public:

  MyClass(float f) 

  { 

         factor=f; 

  } 

  float func1(float f,char a,char b) 

  { 

         printf("MyClass::func1\n"); 

         return f*a/b*factor;      

  } 

  float func2(float f,char a,char b) const 

  { 

         printf("MyClass::func2\n"); 

         return f*a*b*factor; 

  } 

private:

  float factor; 

};

int main(int argc,char *argv[])

{

  float (*pFunction)(float,char,char)=NULL; 

  float (MyClass::*pMemberFunction)(float,char,char)=NULL; 

  float (MyClass::*pConstMemberFunction)(float,char,char)const=NULL; 

  

  float f=10.0; 

  char a='a',b='b'; 

  float result; 

  pFunction=func1; 

  printf("pointer pFunction's address is:%x\n",pFunction); 

  result=(*pFunction)(f,a,b); 

    printf("result=%f\n",result); 

  

  pFunction=&func2; 

  printf("pointer pFunction's address is:%x\n",pFunction); 

  result=pFunction(f,a,b); 

    printf("result=%f\n",result); 

  if(func1!=pFunction) 

         printf("not equal.\n"); 

  

  pMemberFunction=&MyClass::func1; 

  MyClass mc1(0.2); 

  printf("pointer pMemberFunction's address is:%x\n",pMemberFunction); 

  result=(mc1.*pMemberFunction)(f,a,b); 

    printf("result=%f\n",result); 

  pConstMemberFunction=&MyClass::func2; 

  MyClass mc2(2); 

  printf("pointer pConstMemberFunction's address is:%x\n",pConstMemberFunction); 

  result=(mc2.*pConstMemberFunction)(f,a,b); 

    printf("result=%f\n",result); 



  return 0; 

}

运行结果为:

pointer pFunction's address is:400882

func1

result=9.897959

pointer pFunction's address is:400830

func2

result=95060.000000

not equal.

pointer pMemberFunction's address is:400952

MyClass::func1

result=1.979592

pointer pConstMemberFunction's address is:4008f2

MyClass::func2

result=190120.000000

注意:上面的代码还说明了一点就是函数指针的一些基本操作,函数指针没有普通变量指针的算术操作,但是可以进行比较操作。如上面代码所示。

使用类的静态函数成员的函数指针和使用C语言的函数很类似,这里仅仅给出一个例子和其执行结果:

程序代码为:

/*

*Author:Chaos Lee

*Date:2012-02-28

*/

include<iostream>

class MyClass

{

public:

  static float plus(float a,float b) 

  { 

         return a+b; 

  }     

};

int main()

{

  float result,a=10.0,b=10.0; 

  float (*p)(float,float); 

  p=&MyClass::plus; 

  result=p(a,b); 

  printf("result=%f\n",result); 

  return 0; 

}

执行结果为:

result=20.000000

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 整理自计蒜客-CS 112: C++ 程序设计 指针是什么 指针是一个变量,其储存的是值的地址,而不是值本身。指针...
    埠默笙声阅读 675评论 0 3
  • C语言指针的总结 1. 变量 不同类型的变量在内存中占据不同的字节空间。 内存中存储数据的最小基本单位是字节,每一...
    xx_cc阅读 3,562评论 11 39
  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 3,387评论 3 44
  • 指针是C/C++里非常重要的概念,它是内存地址的抽象。指针必须和实际的类型结合起来才有意义,因此,我们会看到指向各...
    qwjcool阅读 1,377评论 4 11
  • 时间总是在过去之后,才显得飞快。今天是2017年的第210天,这一年只剩下1/3。 如果你和我一样,还不能深刻感知...
    申湘黔阅读 198评论 1 2