C语言 | 浅析函数指针与指针函数及其应用

函数指针与指针函数

  • 一、指针简介
    • 1.1、指针定义
    • 1.2、指针变量
      • 1.2.1、指针类型大小
      • 1.2.2、指针注意事项
      • 1.2.3、野指针
  • 二、函数
    • 2.1、函数简介
    • 2.2、函数地址
    • 2.3、回调函数
  • 三、函数指针
    • 3.1、函数指针定义
    • 3.2、函数指针声明
    • 3.3、函数指针的应用
      • 3.3.1函数指针的调用
      • 3.3.2函数的传参
  • 四、指针函数
    • 4.1指针函数的定义
    • 4.2 指针函数的声明
    • 4.3指针函数的应用
  • 五、函数指针与指针函数的区别
  • 六、总结

一、指针简介

什么是指针?相信大家对这个问题其实并不陌生,对指针的概念也不会很模糊,在这里我也大概介绍一下。

1.1、指针定义

指针 其实可以理解为某一对象的内存地址,指针变量就是用来存放内存地址的变量。由此我们发现,其实指针和地址是一种相互对应关系。

1.2、指针变量

指针变量 上面我们谈到指针变量是存放地址的变量,故此我们也可称指针变量我地址变量。在声明指针变量时,我们通常用数据类型符++标识符(即变量名)的格式定义。如int p1,char* p2等形式。
需要说明的是类型说明符表示指针变量所指向变量的数据类型;" * " 表示这是一个指针变量;变量名表示定义的指针变量名,其值是一个地址。例如:int*p1则表示 p1 是一个int型的指针变量,它的值是某个整型变量的地址。

1.2.1、指针类型大小

在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。

以上我们知道指针变量是存储地址的,且其可以指向char型,int型等存储在占据一定空间的实体。以32位机为例,在32位机的存储中,int型,char型变量都分别占据不同的字节。
C语言 | 浅析函数指针与指针函数及其应用_第1张图片
那么指针变量的大小如何呢?通过验证我们发现其实指针类型的占位都是4/8个字节,如下图所示。由此我们知道,地址值在内存中的存储空间占4/8个字节,这也是指针类型的大小了。
C语言 | 浅析函数指针与指针函数及其应用_第2张图片

1.2.2、指针注意事项

我们在对指针进行声明时,切记不可把一个数赋值给指针变量。如下:

    int* p = 200;//erro
    int a = 20;
    int* p1;
    *p1 = a;//erro
    //或者
    int *p2;
    p2 = 200;//erro
    printf("%d\n",*p);
    printf("%d\n",*p1);
    printf("%d\n",*p2);

C语言 | 浅析函数指针与指针函数及其应用_第3张图片
在以上例子中分别定义了一个指针变量 p,p1,p2,但是不能直接把 200 赋值给指针变量 p2,也不可把一个赋值为20的整型变量 a,再赋值给指针变量p1,p1中只能用来存放整型变量的地址,而不能直接把整型变量a赋值给这个指针变量 p1。但可以把a的地址赋值给 p1;即可改成 **p1 = &a;**同时直接在声明指针变量p时也是不可以赋给其200这个整型数值的。
注意,这里虽然只有两个报错,原因是在wind环境下,语法的不严格导致的,在Linux环境下其实是报错的情况。

1.2.3、野指针

在使用指针时,野指针是我们需要避免的一个重要点。

C 语言中指针初始化是指给所定义的指针变量赋初值。指针变量在被创建后,如果不被赋值,他的缺省值是随机的,它的指向是不明确的,这样的指针形象地称为 “ 野指针 ”。野指针是很危险的,容易造成程序出错,且程序本身无法判断指针指向是否合法。
指针变量初始化时避免野指针的方法:可以在指针定义后,赋值 NULL 空值,也可写成如下形式:
p=0 或 p=‘\0’
这两种形式和 p = NULL 是等价的
如int * p;
p = NULL;
上面两行代码的含义是,指针变量 p 被赋值为空。虽然定义了一个指针变量,但是它并不指向任何存储空间。

以上是我们在使用指针时需要注意的内容。同时也应该避免指针越界等问题,这以后细说。

二、函数

2.1、函数简介

这里的函数是同我们理解中的数学函数有一定区别。计算机上的函数一般是指一段可以直接被另一段程序或代码引用的程序或代码。也叫做子程序,或在OOP中称为方法,如java等面向对象的程序设计语言。
在C语言中,函数的定义同大多数变量定义意义,都需要声明,给定类型等且在定义非void型函数时,需要有返回值。值得注意的是,函数名后的()内可进行参数的定义,如定义一个int型的main函数,可接收两个参数时,参照如下定义:

// 定义main函数
int main(int x,int y)
{
	printf("%d\n",x+y);
	return 0;
}

2.2、函数地址

函数有地址吗?我们先来看下面这几行代码。在main函数中,通过调用add函数得到一个返回值,这个返回值被在main函数进行打印。我们知道,在声明一个变量时,内存空间都会为该变量分配一个相应地址值,同样地,在声明函数使,内存也会为函数分配一个地址值,如下代码中的add函数,其在64位机下的内存地址值为0000000000401550;

//定义加法函数add()
int add(int x,int y)
{
	int z = 0;
	z = x + y;
	return z;
}
// 定义main函数
int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n",add(a,b));//30
	printf("%d\n",&add);//0000000000401550,这是个地址值,这里用&add和直接用add是一样的,没区别。不同于数组!
	return 0;
}

综上我们可以很清楚的知道,在函数调用的时候其实就像在主函数中找到了被调函数的地址作为入口进行传参,进而能很好帮我们解决很多问题。而函数指针就是基于这样一个思想来实现指针变量存放函数地址的。

2.3、回调函数

最为编程语言中的一种机制,回调函数就是一个被作为参数传递的函数。在C语言中,回调函数只能使用函数指针实现。
对于回调函数,经典的如qsort()函数,其格式如下:

void qsort(void* base,
                size_t num,
                size_t width,
                int(*cmp)(const void *e1,const void* e2)
                ); 

关于上述代码,这里不作细讲。

回调函数在实际中有许多作用。假设有这样一种情况:我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,可以使用函数指针,并进行回调。

可见回调函数于函数指针的关系极为密切。在此介绍回调函数,当然也是方便下文很好的理解函数指针的一些应用,对于回调函数感兴趣的读者朋友可以查阅相关资料进行了解。
注意:以上说明仅为本次文章做个铺垫,下面进入正题。

三、函数指针

顾名思义, 函数指针就是函数与指针的结合,相信大多数初学者可能也同我一样,开始接触函数指针时会疑惑,这是函数还是指针呢?下面我们进行浅析。

3.1、函数指针定义

函数指针是指向函数的指针变量。本质上“函数指针”就是指针变量,这点是需要强调的,只不过该指针变量指向函数。其类型就由函数类型所决定。函数在C编译过程中,编译器会给每一个函数分配一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数的参数。

3.2、函数指针声明

函数指针的声明问题也是一直困扰大家的一个点,就如同对数组指针的声明一样。需要明确的是,函数指针其本质是指针,所以在声明时应该注意,必须体现指针这一概念。下面我们来看这两个声明的异同。

//int fun(int  ,int  );//此为一个函数,这点是毫无疑问的,关键在于以下两条语句
int  * fun(int  ,  int   );
int  (*fun)(int  , int  );

以上“函数”的声明唯一的不同点在于,fun有无括号的问题,正因为这个小括号,让我们认识了函数和指针组合的强大。
首先,对于以上两种写法的问题,根本在于括号的优先级。第一条语句在声明中,fun没有括号,其优先与fun后面的括号结合,在不考虑 * 的情况下,这是一个指向int型的函数,但有了 * 后, 与int结合,形成了 int
类型,此时这个fun函数指向的是 int* 类型。这时我们称第一条语句为指针函数,其是指向指针的函数,这后续再讨论。
有如上基础,对于第二条语句我们就很容易理解了。通过将 fun 与括号中的 “ * ” 强制组合在一起,表示定义的 fun 是一个指针,然后与下面的 “ ( ) ” 再次组合,表示的是该指针指向一个函数,括号里表示为 int 类型的参数,最后与前面的 int 组合,此处 int 表示该函数的返回值。因此,fun 是指向函数的指针,该函数的返回值为 int。函数指针与指针函数的含义大不相同。函数指针本身是一个指向函数的指针。指针函数本身是一个返回值为指针的函数。
综上,函数指针的声明就显而易见了,其格式为:
返回值的数据类型 ( * 函数名)(参数1,参数2,…);

如果上述感觉有点啰嗦,我们可以这样来理解:
通过上述代码可知函数有一个地址值,那么如果我们想存放这个地址值,该如何存放呢。可能有人会想到的是直接用函数来接收,如int pf(int x,inty) = add;用函数来接收函数,确实是个不错的想法,但我们说地址是一个指针变量,应该用指针来接收地址值,所以很容易想到直接在int 后加上 * 这个符号表示指针,这时有int *pf(int x,inty) = add;但真的是这样吗?有人可能会发现pf(int x,inty) = add;其实是一个函数,返回类型是int*,这认同我们前面认识的其他指针类型一样。没错,这就是关键。由此我们探究出了函数指针的写法,即先让pf变量是个指针,用来接收add函数的地址,再让其指向int型。所以得到了int (*pf)(int x,inty) = add;这样一个完美的写法。

3.3、函数指针的应用

上面我们提到,函数指针有两个用途,即调用函数和做函数的参数,下面我们以此进行一些浅析。

3.3.1函数指针的调用

当我们需要把一个函数作为参数传给其他参数的时候就必须使用函数指针才能很好的完成。下面的程序说明了函数指针调用函数的方法:这段代码中主要执行过程主要是把add函数的地址赋给pf指针,而后通过(*pf)进行解引用找到原来的add函数,再对其进行传参3和5,最后将结果赋给c,再进行打印。得到下图结果。

//定义加法函数add()
int add(int x,int y)
{
	int z = 0;
	z = x + y;
	return z;
}
// 定义main函数
int main()
{
	int (*pf)(int x,int y) = add;
	int c = (*pf)(5,8);
	printf("%d\n",c);//13
	return 0;
}

pf 是指向函数的指针变量,【(*pf)(int x,int y)这个里边的参数 x和y 可以省略】所以可把函数 add() 赋给pf作为 pf 的值,即把add()的入口地址赋给pf,之后就可以用pf对该函数进行调用,实际上pf和add都指向同一个入口地址,不同就是pf是一个指针变量,它可以指向任何函数。在程序中把哪个函数的地址赋给它,它就指向哪个函数。而后用指针变量调用它,因此可以先后指向不同的函数。不过注意,指向函数的指针变量没有++和–运算,用时要小心。

3.3.2函数的传参

上面提到函数指针不仅可以调用,还能进行传参。函数传参的本质就是把需要传的函数当成一个参数,然后传给被调函数使用。下面依然通过代码来演示,并得到下图结果。

//定义加法函数add()
void chuancan(int (*pf)(int x,int y),int x,int y)
{
	printf("这是函数指针传的使用演示\n");
	int c = pf(x,y);
	printf("%d\n",c);//5
}
int add(int x,int y)
{
	int z = 0;
	z = x + y;
	return z;
}
// 定义main函数
int main()
{
	int (*pf)(int x,int y) = add;
	chuancan(pf,2,3);
	return 0;
}

我们把函数pf当做一个参数传到了函数chuancan里面去,也可以实现对函数pf的调用,显然,这样的做法让我们见识到了函数指针的极大魅力。这就意味着,当我们有很多类似的数量极大的函数时,我们怎么管理和使用,这是个棘手的问题,这时候我们可以用函数指针去管理和调用他们,把他们都装进一个函数指针数组,这无疑让我们的算法实现了优化。
C语言 | 浅析函数指针与指针函数及其应用_第4张图片

四、指针函数

当读者朋友读到这一节时,相信你对指针函数已经不再陌生,因为在前文我们也已然提过这一概念。接下来我们将对指针函数作进一步浅析。

4.1指针函数的定义

如你所想,指针函数,其实就是带指针的函数,其本质就是函数,这点并不稀奇,稀奇的是它的返回类型是某一类型的指针。

4.2 指针函数的声明

指针函数的声明如下:
*类型标识符 函数名(参数表) ;

int  * fun(int  ,  int ,... );//这是一个指针函数

下面是关于指针函数的相关声明格式解释。

在 *类型名 函数名(函数参数列表); 格式中,后缀运算符括号“()”表示这是一个函数,其前缀运算符星号“ * ”表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个“指向返回值为…的指针(地址),“类型名” 表示函数返回的指针指向的类型”。
“(函数参数列表)”中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下:
int pfun(int, int);
由于“
”的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。即:
int *(pfun(int, int));
接着再和前面的“ * ”结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数

相信到此读者朋友应该对指针函数的定义,声明及其注意点有了深度的认识,需要注意的是:
函数指针声明为指针,它与变量指针不同之处是,它不是指向变量,而是指向函数
下面将对指针函数应用方面作进一步的浅析。

4.3指针函数的应用

指针函数既然本质上是函数,那么其用法与函数的一般用法基本无异。都需要对其进行声明,调用等。请看一下例子,结果如下图:

char * my_strcat(char* dest,const char * src)
{
    char* ret = dest; 
    assert(dest != NULL);
    assert(src);
    //1.找到目的字符串的\0
    while(*dest != '\0')
    {
        dest++;
    }
    //2.追加
    while (*dest++ = *src++)
    {
        ;/* code */
    }
    return ret;
    
}
int main()
{
    char arr1[30] = "abcdefghi";//这里目的地要足够大,否则追加时容易出错
    char arr2[] = "abtc";
    char arr11[] = "abcdefghi";
    char arr22[] = "abtc";
    char* arr3 = my_strcat(arr1,arr2);
    printf("这是自记实现的追加字符串结果:%s\n,这是系统调用strcat函数追加的结果:%s\n",arr3,strcat(arr11,arr22));
    return 0;
}

在本例中,声明char * my_strcat(char* dest,const char * src)这样一个返回值类型为char*的指针函数,参数为char* dest,const char * src,该函数用来实现对两个字符串的后缀字符追加问题。当我们在主函数调用my_strcat函数时,其实和一般函数调用并无区别,值得一提的是,当我们想要把被调函数返回给某个变量时,记住返回类型即可,如char* arr3 = my_strcat(arr1,arr2);C语言 | 浅析函数指针与指针函数及其应用_第5张图片
指针函数的返回类型可以是任何基本类型和复合类型。返回指针的函数的用途十分广泛。事实上,每一个函数,即使它不带有返回某种类型的指针,它本身都有一个入口地址,该地址相当于一个指针。比如函数返回一个整型值,实际上也相当于返回一个指针变量的值,不过这时的变量是函数本身而已,而整个函数相当于一个“变量”。
当然了,在函数调用过程中,我们会发现,每当封装一个函数,相当于把这个函数给定死了,今后再使用时发现只能使用固定返回类型的函数,这是令人头大的问题,为此c语言还是很友好的,在这里我们简单介绍遇到这种情况该如何解决。
一般而言,为了让被调函数具有一般通用性,我们可以将其返回值类型声明为 void * ,这样就极大方便我们在今后的使用了。

五、函数指针与指针函数的区别

经过上述探索,我们悄然发现,函数指针和指针函数的区别依然明了。在这里需要总结一下即可:
1、指针函数: 即带指针的函数,其本质是一个函数,函数返回是某一类型的指针。声明如ret * fun(ret ,ret ,...);
2、函数指针: 即指向函数的指针变量,其本质是一个指针变量。指向函数的入口地址,可以通过它来调用函数。声明如ret (*fun)(ret ,ret ,...);
综上,二者在定义,写法及用法方面都存在不同,至于对二者更进一步的学习,还请各位读者朋友查询相关资料进行深度学习了。鉴于个人技术学浅,存在不足之处,欢迎指教。

六、总结

感 谢 您 的 阅 读 !

你可能感兴趣的:(c语言,c++,开发语言,面试,经验分享)