函数与指针

指针函数

函数内部数据是地址,需要传递给调用函数,可以将指针返回给函数
或者也可以通过双向传递参数来获取地址,但一般不推荐
不用函数参数双向传递的原因在于:
一旦调用的函数中对指针的值做了修改,原指针的值并没有变化(此时相当于传值调用),那么程序中接下来围绕指针的操作就与愿指针指向的值无关了

返回指针的函数一般定义格式为:

数据类型 *函数名(参数列表){函数体;} 数据类型为返回的指针指向的数据类型,同时也要return指针类型

函数返回指针示例程序1:
实现匹配函数match:在输入字符串中查找一个给定字符,如果找到则从该字符开始打印余下的子字符串,及该字符是字符串的第几个字符,- 否则输出"no match found"

  • 由于函数的功能一般是独立的,函数的输出一般通过函数双向传递的参数或返回值获得,因此函数的输出一般是函数计算结果反馈给调用者
  • 至于调用者是把结果输出到屏幕还是作为其他函数的输出,由调用者根据需要而定,除非必要一般不在函数内部通过打印函数这种显示语句输出数据
#include<stdio.h>
#include<string.h>
char *match(char c,char *sp);          match函数原型,一个字符串,一个查找字符,返回值为字符串地址
int main()
{
    char s[80],ch,*p;
    int pos;
    gets(s);
    ch=getchar();
    p=match(ch,s);                     //调用match函数
    if(p)                              //如果指针不为空
    {
        pos=strlen(s)-strlen(p)+1;     //原字符串长度减子字符串长度的差再加上字符本身就表示该字符在字符串中是第几个字符
        printf("%s %d\n",p,pos);       //输出子字符串和给定字符在字符串中的位置
    }
    else printf("no match found");
}
char *match(char c, char *sp)
{
    int count=0;
    while(c!=sp[count]&&sp[count]!='\0')  //如果c与字符串中的字符不相等,并且不是结束字符,则指针向下移动
    count++;                              //记录指针向下移动次数,循环结束则指针移动结束
    if(sp[count])                         //如果此时指针指向的字符不为空则说明在字符串中匹配到了该字符
    return(&sp[count]);                   //所以返回此时的指针
    else return NULL;                     //如果没有匹配到,返回NULL
}

除了对字符,数组操作经常存在获取地址信息的情况外,如果函数内部有动态分配空间等操作也经常需要返回地址信息
函数返回指针示例程序2:获取动态内存,通过形参双向传递地址信息

#include<stdio.h>
#include<stdlib.h>
void Getmemory(int n,int **p);            //定义一个二重指针形参**p用来对指针值进行双向传递
int main()
{
    int n,*add=NULL;                      //令主调函数的指针add初值为空,防止后续调用函数结束后给*add赋值时才分配空间,从而引起混淆
    scanf("%d",&n);
    Getmemory(n,&add);                    //调用Getmemory函数
    *add=1;                               //向add指向的空间中写入数据
    printf("%p是%d",add,*add);
}
void Getmemory(int n,int **p)
{
    *p=(int*)malloc(n*sizeof(int));       //此时二重指针p指向实参add的地址,将动态分配的地址空间赋给add
    return;
}

注意局部变量的地址一般不作为函数的返回值,例:

    int *test();
    int main()
    {
        int *t1,*t2;
        t1=test();
        *t1=10;                              //修改t1指针指向的变量a的值为10
        t2=test();                           //再次调用test(),a又被赋值为5
        printf("t1=%p,*t1=%d\n",t1,*t1);     //输出结果:t1=0x7ffeec1a55dc,*t1=5         虽然结果为5,但此时t1指向的a其实已经被回收了
        printf("t2=%p,*t2=%d\n",t2,*t2);     //输出结果:t2=0x7ffeec1a55dc,*t2=983229963 此时test()函数调用结束,a的内存空间被释放
    }                                                                                //t2指向的空间中是一个随机数
    int *test()
    {
        int a=5;
        return &a;
}

局部变量作为返回值时,一般是系统申请一个临时对象存储局部变量,也就是找个替代品,这样系统就可以回收局部变量,返回的只是个替代品。但如果是指针,因为返回的局部变量是地址,地址虽然返回了但所指向的内存中的值已经被回收,主函数再去调就可能有问题。

函数指针

函数指针:指向函数的指针
在c中,函数本身不是变量,但可以定义指向函数的指针。一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似,可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,通过指针变量就可以找到并调用该函数,这种类型的指针即为函数指针,可以被赋值、存放在数组中、传递给函数以及作为函数返回值等。

函数指针定义的基本形式为:返回类型 (*指针名)(参数列表);

参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。注意()的优先级高于*,因此第一个括号不能省略,尤其如果在函数参数中省略则通常是一个非法的表示(先计算括号内再转换为int*),如果写作

返回值类型 *指针名(参数列表);

就成了指针函数原型,它表明函数的返回值类型为指针,指针名实际上是函数名

函数指针示例程序:用指针来实现对函数的调用

#include<stdio.h>
using namespace std;
int max(int a, int b)
{
    return a>b?a:b;
}
int main()
{
    int (*pmax)(int,int)=max;              //因为函数名代表首地址,本质上也是个指针常量,所以将整型函数赋值给整型指针pmax
    printf("%d\n",pmax(1,2));              //此时pmax存储的就相当于函数首地址指针,因此可以直接用pmax()调用函数
    printf("%d\n",(*pmax)(1,2));           //*pmax相当于函数的内容,因此*pmax()也可以调用函数
}

可以使用typedef来声明函数类型和函数指针类型,如:

typedef 返回类型 函数类型名(参数列表);
typedef 返回类型 *指针函数类型名(参数列表);
typedef 返回类型 (*函数指针类型名)(参数列表);

注意c语言的定义中是没有函数类型的,因此它的函数变量会自动退化成函数指针(不过在c++中好像是可以的,在这里主要说明的是形式上的相似性)
使用typedef定义函数指针,代码看起来更简洁,也更不容易出错,并且可以将定义的函数指针类型作为返回值,例:

int add(int a, int b) {
    return a + b;
}
typedef int (PTypeFun1)(int, int); // 声明一个函数类型
typedef int (*PTypeFun2)(int, int); // 声明一个函数指针类型
int (*padd)(int, int); // 传统形式,定义一个函数指针变量 
int (*padd)(int, int); // 传统形式,定义一个函数指针变量 
// PTypeFun1 getfuncptr(){  // 返回函数类型不可以的
//     return padd;
// }
PTypeFun1*getfuncptr(){  // 返回函数类型不可以的
    return padd;
}
int usefuncptr(PTypeFun1 p1,PTypeFun2 p2){
    return p1(1,2)+p2(1,2);
}
int main() {
    PTypeFun1 *pTypeAdd1 = add;
    PTypeFun2 pTypeAdd2 = add;
    padd = add;
    printf("%d\n",pTypeAdd1(1,2)); //pTypeAdd1相当于退化成了函数指针类型
    printf("%d\n",pTypeAdd2(1,2));  
    printf("%d\n",usefuncptr(getfuncptr(),pTypeAdd2));
    return 0;
}

复杂声明

c语言常常因为声明的语法问题而受到人们的批评,特别是涉及函数指针的语法。c语言的语法力图使声明和使用相一致,对于简单的情况,c的做法是很有效的,但如果情况较为复杂则容易让人混淆。
主要原因是c的声明不能从左至右阅读,而且使用了太多的圆括号,如:

int* f();int(* pf)();其中f是一个函数,它返回一个指向int类型的指针,而pf是一个指向函数的指针,该函数返回一个int类型的对象

f和pf之间的含义差别说明:
*是一个前缀运算符,其优先级低于(),因此在声明中必须使用圆括号以保证正确结合顺序。实际中其实很少用到过于复杂的声明,但是懂得如何理解以及使用这些复杂声明是很重要的。简化复杂声明的方法有很多,主要有两种方法:

  • 一种较好的方法是使用typedef通过简单的步骤合成
  • 另一种方法:使用两个程序,一个程序(dcl)用于将正确的c声明转换为文字描述,另一个程序完成文字描述顺序的转换,从左向右阅读(direct-dcl)
    通过程序将声明转换为文字说明,就可以充分理解声明的含义

使用效果例:

char ** argv
argv:pointer to pointer to char
int (* daytab)[13]
daytab:pointer to array[13] of int
int * daytab[13]
daytab:array[13] of pointer to int
void * comp()
comp:function returning pointer to void
void (* comp)()
comp:pointer to function returning void
char (* (* x())[])()
x:function returning pointer to array[] of pointer to function returning char
char(* (* x[3])())[5]
x:array[3] of pointer to function returning pointer to array[5] of char

dcl是基于声明符的语法编写的,其简化的语法形式为:

dcl: 前面带有可选的* 的direct-dcl
direct-dcl: name|(dcl)|direct-dcl()|direct-dcl[可选的长度]

简而言之:-

  • 声明符dcl是前面可能带有多个*的direct-dcl(因为多个指针连在一起可以理解为指向…指向…指向的指针)
  • direct-dcl可以是name、由一对圆括号括起来的dcl、后面有一对圆括号的direct-dcl、后面有用方括号括起来的表示可选长度的direct-dcl

该语法可对c的声明进行分析,如:

(*pfa[])();

按该语法分析,pfa将被识别为一个name,从而被认为是一个direct-dcl,然后pfa与[]结合也是一个direct-dcl,接着** pfa[]被识别为一个dcl(非direct-dcl),然后(* pfa[])被识别为一个direct-dcl,最后(* pfa[])()是一个direct-dcl

程序dcl的核心是两个函数:dcl,dirdcl,它们根据声明符的语法对声明进行分析

由于语法是递归定义的,所以在识别一个声明的组成部分时,这两个函数是相互递归调用的,称该程序是一个递归下降语法分析程序

enum {NAME,PARENS,BRACKETS};
void dcl(),dirdcl();
char token[MAXTOKEN],name[MAXTOKEN],datatype[MAXTOKEN],out[MAXLINE];
int gettoken(),tokentype;
//tokentype:token的类型,定义了三种类型分别是NAME:标识符,PARENS:函数,BRACKETS:数组
void dclmain()
{
    while(gettoken()!=EOF)
    {   
        strcpy(datatype,token);
        *out='\0';
        dcl();
        if(tokentype!='\n')cout<<"syntax error"<<endl;
        cout<<name<<":"<<out<<' '<<datatype<<endl;
    }
}
void dcl()
{
    int ns=0;
    while(gettoken()=='*')ns++;
    dirdcl();
    while(ns-->0)strcat(out," pointer to");
}
void dirdcl()
{
    int type;
    if(tokentype=='(')
    {
        dcl();
        if(tokentype!=')')cout<<"error:missing ')'"<<endl;
    }
    else if(tokentype==NAME)strcpy(name,token);
    else cout<<"error:expected name or (dcl)"<<endl;
    while((type=gettoken())==PARENS||type==BRACKETS)
    if(type==PARENS)strcat(out," function returning");
    else
    {
        strcat(out," array");
        strcat(out,token);
        strcat(out," of");
    }
}

该程序的目的旨在说明问题,并不想做得尽善尽美,所以对dcl有很多的限制,它只能处理类似于char或int的简单数据类型,无法处理函数中的参数类型或类似于const这样的限定符,不能处理带有不必要空格的情况。由于没有完备的出错处理,因此它也无法处理无效的声明。

int gettoken()
{
    int c;
    char *p=token;
    while((c=getch())==' '||c=='\t');
    if(c=='(')
    {
        if((c=getch())==')')
        {
            strcpy(token,"()");
            return tokentype=PARENS;
        }
        else
        {
            ungetch(c);
            return tokentype='(';
        }
    }
    else if(c=='[')
    {
        for(*p++=(char)c;(*p++=(char)getch())!=']';);
        *p='\0';
        return tokentype=BRACKETS;
    }
    else if(isalpha(c))
    {
        for(*p++=(char )c; isalnum(c=getch());)*p++=(char )c;
        *p='\0';
        ungetch(c);
        return tokentype=NAME;
    }   
    else return tokentype=c;
}

如果不在乎生成多余的圆括号,则另一个方向的转化要容易一些
为了简化输入将"x is a function returning a pointer to an array of pointers to functions returning char"
(x是一个函数,返回一个指针,该指针指向一个数组,数的元素是指针,这些指针分别指向多个函数,这些函数的返回类型是char)
程序undcl将该形式转化为

char (* (* x())[])()

由于对外部输入的格式进行了限定,所以可以重用上方的gettoken()函数和外部变量

void undclmain()
{
    int type,braket;
    char temp[MAXTOKEN];
    while(gettoken()!=EOF)
    {
        strcpy(out,token);
        while((type=gettoken())!='\n')
        if(type==PARENS||type==BRACKETS)strcat(out,token);
        else if(type=='*')
        {
            nexttoken=YES;
            braket=gettoken();
            if(braket==PARENS||braket==BRACKETS)
                sprintf(temp,"(*%s)",out);
            else if(braket!='\n')sprintf(temp,"*%s",out);
            else break;
            strcpy(out,temp);
        }
        else if(type==NAME)
        {
            sprintf(temp,"%s %s",token,out);
            strcpy(out,temp);
        }
        else cout<<"invalid input at "<<token<<endl;
        cout<<out<<endl;
    }
}