目录
上一篇复习了第三次的8道编程题,前面我们讲的都是指针的基础,这一节开始是高级指针的内容。
函数指针
函数指针的定义
在C语言中,一个函数总是占用一段连续的内存区,而函数名就是该函数所占内存区的首地址。
把函数的这个首地址(函数入口地址)赋予一个指针变量,使该指针变量指向该函数。然后通过指针变量就可以找到并调用这个函数。我们把这种指向函数的指针变量称为“函数指针变量”。
函数指针变量定义的一般形式为:
类型说明符 (*指针变量名)();
例如:
int (*pf)( );
表示pf是一个指向函数入口的指针变量,该函数的返回值(函数值)是整型。
这样定义是如何确定它就是函数指针的呢?我们C语言中有个法则叫“右左法则”,意思是我们先分析右边再分析左边,我们以函数指针的定义为例:int (*pf)( ); 先看p这个变量的右边,是个括号,我们只能转向看p的左边,是个*号,说明p是个指针变量名。括号内分析结束,出了这个括号后一样先分析右边,是个括号,那说明这个是个函数调用运算符,也就是说p这个指针一定是指向了一个函数,而这个函数没有参数,再看左边是个int,说明这个函数的返回值是个int型。
函数指针的调用
例如:
int func(int a, int b)
{
return a + b ;
}
int main()
{
int (*pf)(int, int);
pf = func;//第一步:将函数名(函数入口地址)赋值给指针
pf(); //第二步:通过函数指针来调用函数,等价于func();
return 0;
}
注意:被赋值给指针的返回值类型一定要跟函数指针定义时返回值类型一样,否则会出现类型不兼容。
补充:关键字typedef重命名
比如当我们嫌每次都敲unsigned char太长了,我们可以将它重命名:typedef unsigned char uchar; 将unsigned char重命名为uchar,之后我们写uchar即表示unsigned char。
同样如果我们嫌函数指针的类型int (*指针变量名 ) ( ); 每次定义太麻烦了,我们可以重命名:typedef int(*T)(int,int); 表示用T来表示函数指针类型int (*指针变量名 ) (),之后我们写函数指针类型就可以写为:T k; T表示函数指针类型,k是指针变量,等价于 int(*k)(int,int)。
笔试题20
下面两个表达式什么意思?
int *(*(*fp)(int))[10];
int *(*(*array[5])())();
我们用“右左法则”分析一下:
int *(*(*fp)(int))[10];先从fp开始分析起,它的右边是个括号,所以看向它的左边是*号说明它是个指针,出来后fp它的右边是个括号且有参数,说明是个函数调用语句,fp指向函数,函数的参数是个int型,然后fp的左边是个*说明这个函数的返回值是个指针;
(*(*fp)(int))出来后右边是个中括号,说明是个数组定义语句,数组有10个元素。再往左边看是个int*型,说明每个数组元素都是int型的指针。
int *(*(*array[5])())();先从array开始分析起,它的右边是个[5]说明是个array是个数组,有5个元素,array的左边是个*,说明每个元素是个指针。(*array[5])出来右边是个()括号,说明是个函数定义语句,每个元素指向函数1,函数1没有参数,(*array[5])的左边是个*号,说明函数1的返回值是个指针,(*(*array[5])())出来右边是个()说明是个函数定义语句,说明函数1的返回值指向函数2,函数2没有参数,(*(*array[5])())的左边是个int*,说明函数2的返回值是个int*的指针。
函数指针在实际应用中比较少,但是在笔试的时候会考到。一般它会在回调函数的时候用到。
回调函数
回调函数:把函数的名字作为另一个函数的参数
用处:可以修改函数的功能
我们将之前写过的冒泡排序的代码改成这样,就能实现一个函数既能实现从大到小、也能实现从小到大排序的功能:
#include <stdio.h>
int less(int x,int y)
{
return (x>y)?1:0;
}
int greater(int x,int y)
{
return (x<y)?1:0;
}
void sort(int*a,int size,int(*p)(int,int))
{
int i,j;
//冒泡排序
for(i=0;i<size-1;i++)
{
for(j=0;j<size-1-i;j++)
{
if(p(a[j],a[j+1]))
{
int tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
}
}
int main()
{
int a[10]={0};
int num;//循环次数
//获取数组元素
int size=sizeof(a)/sizeof(a[0]);
for(num=0;num<size;num++)
{
scanf("%d",&a[num]);
}
sort(a,size,greater);
//遍历数组
for(num=0;num<size;num++)
{
printf("%d ",a[num]);
}
return 0;
}
运行结果:
这样我们只需要改这里
greater就是从到大到小排列,改成less就就是从小到大排列
回调函数还是比较重要的,到后面我们学习C++的时候还会涉及到。
指针函数
1、指针函数是指返回值是指针的函数。
在C语言中允许一个函数的返回值是一个指针(地址),这种返回指针值的函数称为指针型函数,简称指针函数。
2、定义指针型函数的一般形式为:
类型说明符 *函数名(形参表)
{
/*函数体*/
}
其中函数名之前加了“*”号表明这是一个指针型函数,即返回值是一个指针。类型说明符表示了返回的指针值所指向的数据类型。
注意:不能返回局部变量的地址!!!
比如这样写是错的:
char *init()
{
char str[32]={0};
return str;
}
int main()
{
char *s=init();
strcpy(s, ”helloworld’);
return 0;
}
因为当char *init()这个函数返回str数组的地址给char *s时,由于char str[32]是个局部变量,所以它的空间会被释放掉,所以即使s记住了这个数组的地址也没用,因为s所指向的那空间已经不存在了。
这段代码我们可以这样写:
char *init()
{
char *str=(char*)malloc(sizeof(char)*32);
return str;
}
int main()
{
char *s=init();
strcpy(s, ”helloworld’);
free(s);
return 0;
}
因为这样写是在堆空间给数组申请空间的,不会被释放掉。
或者定义成全局变量运行完函数体后也不会被立即释放掉:
char str[32]={0};
char *init()
{
return str;
}
int main()
{
char *s=init();
strcpy(s, ”helloworld’);
return 0;
}
int(*p)() 和 int *p()区别?
int (*p)()定义的是一个变量。表示p是一个指向函数入口的指针变量,该函数的返回值是整型,(*p)的两边的括号不能少。 因此int(*p)() 是一个函数指针。
int *p()则不是变量说明而是函数声明。说明p是一个指针型函数,其返回值是一个指向整型量的指针,*p两边没有括号。作为函数说明,在括号内最好写入形式参数,这样便于与变量说明区别。 因此int *p() 是一个指针函数。
对于指针型函数定义,int *p()只是函数头部分,一般还应该有函数体部分。
指针数组
指针数组是数组!一个数组的元素值为指针,则该数组是指针数组。
指针数组的所有元素都必须是具有相同存储类型和指向相同数据类型的指针变量。
指针数组说明的一般形式为:
类型说明符 *数组名[数组长度]
其中类型说明符为指针所指向的变量的类型。
例如: int *pa[3];
表示pa是一个指针数组,它有三个元素,每个元素值都是一个指针,指向整型变量。
数组指针
数组指针是一个指针!该指针指向数组(一维或二维都可以)。
例如:
int (*p)[5];
可以这么理解,p是一个指针,指向一个数组,该数组有5个元素,每个元素都是整型。
注意:
这样写是错误的:int b[5]={0}; int*p=&b;是错误的,因为&b取出来的是整个数组b的地址,而int*p是表示p指向int型的数,是4个字节,而&b一次取出来了20个字节,类型不兼容。
这样写是对的:int (*p)[5]=&b;此时这个p指向的是整个数组b
指针和二维数组
通过指针访问二维数组
示例:
int array[3][4]={{1,2,3,4}, {2,3,4,5}, {3,4,5,6}};
int *s1[3]={array[0], array[1], array[2]};
遍历:
示例:
int i;//行
Int j;//列
for(i=0; i<sizeof(s1)/sizeof(s1[0]);i++)
{
for(j=0;j<sizeof(array[0])/sizeof(array[0][0]);j++)
{
printf(“%lu”,*(s1[i]+j)); //等价于s[i][j]
}
printf(“\n”);
}
注意:不能写成j<sizeof(array[0])/sizeof(s1[0]),因为sizeof(s1[0])算出来是一个指针的大小
通过数组指针访问二维数组
int(*s1)[4]=array;//s1指向array的第一行,第一行相当于一个一维数组,有4个元素,每个元素是int型
遍历:
for(i=0; i<3;i++)
{
for(j=0;j<4;j++)
{
printf(“%d ”, *(*(S1+i)+j);
}
printf(“\n”);
}
注:**S1的含义:*S1取出来S1首元素的地址,
然后再**S1取出来首元素的值
用指针表示二维数组并访问
int *S1=&array[0][0];
for(i=0;i<12;i++)
{
printf(“%d ”,s1[i]);
}
printf(“\n”);
地址等级
0级地址:没有地址,就是元素本身;
一级地址:元素的地址,4字节;
二级地址:行的地址,16字节;
三级地址:数组的地址,48字节;(三行四列,12个元素,每个元素4字节)
0级地址:
a[0][0]:不存在地址,a[0][0]+1就是数值上的加1;
一级地址:
&a[0][0],a[0], *a;
二级地址:
a, &a[0], &a[1];
三级地址:
&a;
二维数组小结
对于二维数组a[3][4]:
注:*a的“*”取值号只是修改了含义,不是真正的访问内存中的数值,原本a是表示二维数组首行的地址,*取值后变成首行首元素的地址。我们可以这样记忆:原本a是二维数组的首行的地址,而一行的地址是用本行的第一个元素的地址表示的,*a里面存放的是首行的地址,即*a就是首行首元素的地址。*a的“*”主要是将二级地址变成一级地址。
1、不要把&a[i]简单的理解为a[i]的物理地址,因为并不存在a[i]这个变量。它只是一种地址的计算方法,能得到第i行的地址。
2、&a[i]和a[i]的值是一样的,但它的含义却不同。 &a[i]或a+i指向行,而a[i]或*(a+i)指向列。当下标j=0时,&a[i]和a[i]+j的值相等。*(a+i)只是a[i]的另一种表示形式。在二维数组中,a+i=a[i]=*(a+i)=&a[i]=&a[i][0]即它们的地址值是相等的。
指针的指针
指针的指针:指向指针的指针
下面这段代码存在什么问题?
void init_s(char *s1)
{
s1 = malloc(1024);
}
int main()
{
char *s;
init_s(s);
strcpy(s, “helloworld”);
return 0;
}
程序会死掉,因为 init_s(char *s1)的函数体运行完之后会释放掉空间,根本就无法将字符串拷贝进去。
可以改成:
void init_s(char **s1)//形参改成指针的指针
{
s1 = malloc(1024);//将申请的空间的地址赋值给s1指向的那块内存
}
int main()
{
char *s;
init_s(&s);//取指针的地址传过去
strcpy(s, “helloworld”);
return 0;
}
这是我之前的博文中也讲过的:如果要修改实参的值,一定要取地址!
命令行参数
main (int argc,char *argv[ ])
argc(第一个形参)必须是整型变量,
argv( 第二个形参)必须是指向字符串的指针数组。
argc参数表示了命令行中参数的个数(注意:文件名本身也算一个参数);
argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。 针数组的长度即为参数个数。
示例:
#include <stdio.h>
int main(int argc, char*argv[ ])//argc是参数的个数,char*argv[ ]保存了参数的地址
{
int i;
for(i=0;i<argc;i++)
{
printf("第 %d 个参数是%s\n",i,argv[i]);
}
return 0;
}
运行结果:
下节开始讲结构和联合!
如有问题可评论区或者私信留言,如果想要进交流群请私信!