目录
前言
1. 字符指针
2. 指针数组
3. 数组指针
4.&数组名VS数组名
5. 函数指针
7. 回调函数
我们知道指针学习是C语言必不可少的一部分,大家如果对指针还不是很了解,可以回看博主之前的文章:《c语言一篇文章让你进一步了解指针》,接下来我们一起来学习一下指针的一些进阶小知识
在指针的类型中我们知道有一种指针类型为字符指针 char *
一般的使用方法:
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'w';
return 0;
}
还有另一种使用方式:
int main()
{
const char* pstr = "hello world";
printf("%s\n", pstr);
return 0;
}
这种使用方式很容易让人觉得是把字符串“hello world”放到字符指针里了,但是本质是把字符串“hello world”的首字符地址放到了pstr中
有了上面的理解,我们来看看下面的笔试题:
#include
int main()
{
char str1[] = "zhangdeshuai";
char str2[] = "zhangdeshuai";
const char* str3 = "zhangdeshuai";
const char* str4 = "zhangdeshuai";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
这里最终的输出为:
【解释】
这里我们要分清楚 str1 str2 和 str3 str4 的差别
- str1和str2:用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,所以str1和str2其实是分别创建了自己的空间来储存“zhangdeshuai”这个字符串,因此str1和str2的地址肯定是不相同的
- str3和str4:str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存,所以str3和str4指向的的地址是相同的
这里还要注意:&str3!= &str4 ,str3和str4是指针,他们的地址是不相同的,仅仅是他们指向内容的地址是相同的
在博主的《c语言一篇文章让你进一步了解指针》一文中,我们已经对指针数组有了一个基本的了解,我们来复习一下下面的指针数组是什么意思?
int* arr1 [ 10 ]; // 整形指针的数组char * arr2 [ 4 ]; // 一级字符指针的数组char ** arr3 [ 5 ]; // 二级字符指针的数组
#include
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr[] = { arr1,arr2,arr3 }; //指针数组
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
return 0;
}
首先我们要先明白数组指针到底是数组还是指针?
我们类比一下:
整形指针:指向整形数据的指针
字符指针:指向字符数据的指针-
....
数组指针:那么就是指向数组的指针
所以数组指针是指针
下面的代码哪一个是数组指针呢?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
【注】:
int (*p)[10];p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr; //把数组arr的地址赋值给数组指针p
return 0;
}
但是我们用数组指针维护一维数组就是多此一举,因为简单的整形指针就可以遍历整个一维数组
接下来我来讲解一下数组指针通常的用法:
#include
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for(i=0; i|
数组名arr表示数组的首元素地址,而二维数组可以看作是由多个一维数组组成,所以arr其实相当于第一行的地址,而数组指针就是指向数组的指针,所以可以用数组指针来接收 ,a[i]就相当于*(arr+i), 即第i行,要访问第i行的某个元素,加j就好了,*(*(arr+i)+j) , 即arr[i][j]
【注】:切记用指针数组维护二维数组时,数组指针 int(*p)[5] 方括号中的数字一定要和二维数组的列数相同
对于下面的数组:
int arr[10] ;
arr和&arr分别是啥?
我们来看一段代码:
#include
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
【结果】:
arr = 00EFF920
&arr = 00EFF920
可见数组名和&数组名打印的地址是一样的,那他们真的一模一样吗?
我们保留这个问题再看一段代码:
#include
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
根据上面的代码我们发现,其实 &arr 和 arr ,虽然值是一样的,但是意义应该不一样的。实际上: &arr 表示的是 数组的地址 ,而不是数组首元素的地址。本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型数组的地址 +1 ,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是 40.
函数指针顾名思义,就是指向函数的指针,那函数的地址是如何保存的呢?
下面 pfun1 和 pfun2 哪个有能力存放 test 函数的地址?void ( * pfun1 )();void * pfun2 ();
【答案】:
函数指针保存的是函数的地址,那怎么才能得到函数的地址呢?是&函数名吗?下面我们来验证一下:
#include
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们发现,对于函数来说,&函数名和函数名都是函数的地址
接下来我们看看怎样通过函数指针来调用函数:
#include
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*pf)(int ,int)=add;
int ret =(*pf)(2,3);
printf("%d\n",ret);
ret =pf(2,3);
printf("%d\n",ret);
ret =(******pf)(2,3);
printf("%d\n",ret);
return 0;
}
上述代码输出的结果都为5,一般情况我们会想pf指向的是函数,那我想调用函数只要对pf解引用就可以了,这种操作是正确的,但我们在想,如果不用函数指针我们调用函数一般会这样写:add(2,3) ,我们都知道add是函数的地址,那pf里存放的不也是函数的地址吗?那是不是可以把pf前的*号去了呢?结果显示直接用pf(2,3)也是正确的
【注】:用函数指针调用函数时前面的*号就是一个摆设,加一个*和加几个*都不会影响函数的调用,故我们可以直接去掉*,这样还可以使代码更清晰
下面我们来看两个由于的代码:
(*(void (*)())0) ()
【解释】:
- void(*)() 函数指针类型,这个函数没有参数,没有返回值
- ( void(*)() ) 0 这是将0强制转换为函数指针类型,0是一个地址,也就 是说一个函数存在首地址为0的一块区域内
- (*(void (*)())0) () 解引用0地址的函数
void (*signal(int , void(*)(int)))(int);
- signal(int , void(*)(int)):signal首先和()这个括号结合,由此可以看出signal是函数名;
- signal(int , void(*)(int)):signal函数的第一个参数的类型是int(整形),第二个参数的类型是函数指针类型,由此该函数指向的第一个参数是int,第二个为返回类型是void的函数;
- signal函数的返回类型也是一个函数指针,这个函数指针指向的是一个参数为int,返回类型是void的函数;
6. 函数指针数组
int *arr[10];
//数组的每个元素是int*
int (*parr1[10])();
#include
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
switch (input)
{
case 1:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = add(x, y);
printf( "ret = %d\n", ret);
break;
case 2:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = sub(x, y);
printf( "ret = %d\n", ret);
break;
case 3:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = mul(x, y);
printf( "ret = %d\n", ret);
break;
case 4:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = div(x, y);
printf( "ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
breark;
default:
printf( "选择错误\n" );
break;
}
} while (input);
return 0;
}
上述代码实现了一个简单的计算器功能,虽然代码结构很清晰,但是这个代码的内容有大量重复的信息,当其功能越来越多时,switch语句也会越来越长,出现了大量冗余,我们仔细观察,会发现实现加减乘除四个函数的返回值即形参类型是一摸一样的,下面我们就可以利用函数指针数组来维护这几个函数,代码就会变得相对简洁:
#include
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0;
}
总结一下:函数指针数组的一般用途为:转移表
要注意,使用函数指针数组也是有前提条件的,被保存在函数指针数组的这几个函数必须要有相同的返回类型,形参的类型,个数也要一摸一样,正确使用函数指针数组,可以减少一部分冗余的代码,比switch case语句的效率高,当函数个数很多时,转移表的优点就表现出来了
回调函数就是一个通过 函数指针 调用的函数。如果你把函数的指针(地址) 作为参数 传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调函数的一个经典例子就是qsort函数,接下来我们来介绍一下这个函数:
qsort函数是一个可以对任何类型进行排序的一个函数,其形参base指向的是需要被排序的内容,由于创造qsort的程序员也不知道我们排序内容是什么类型的,所以把base定义为void类型的指针,num为排序内容元素的个数,size为单个元素的大小,最后一个参数是一个函数指针,指向的是可以比较被排序内容大小的一个函数,这个比较函数需要我们自己写出来,因为只有我们自己才知道排序内容是什么类型的
#include
int int_cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
int_cmp函数被作为参数传给了qsort函数,而qsort通过函数指针调用了int_cmp函数,这便是回调函数
下文预告:
使用回调函数模拟实现qsort函数
(采用冒泡排序的方式)