写在前面
本篇文章主要讲解函数指针和回调函数的理解和使用,属于较难的指针知识。如果想要学习指针简单和数组的相关知识,可以进入下面的传送门。
指针初阶知识传送门
☀️数组知识传送门☀️
下来就是文章内容了,端上小板凳开始阅读吧
在指针的类型中有一种指针类型为字符指针char*
一般使用方法:
使用字符指针指向字符变量进行读写操作
int main()
{
char c = 'w';
char* pc = &c;
*pc = 'w';
return 0;
}
另外一种使用方式:
int main()
{
char* pstr = "hello world";//这里是把一个字符串的首地址放到pstr指针变量里
printf("%s\n", pstr);
return 0;
}
代码char* pstr = "hello world";
特别容易理解为是把字符串hello world
放到字符指针pstr
里
了,但是其本质是把字符串hello world
的首字符h
的地址放到了pstr
中。
一道相应考点的面试题
#include
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
char* str3 = "hello bit.";
char* str4 = "hello bit.";
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;
}
输出结果:
这里
str3
和str4
指向的是一个同一个常量字符串。C/C++
会把常量字符串存储到单独的一个内存区域。当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1
和str2
不同,str3
和str4
不同。
首先看一段代码:
#include
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出结果:
输出的是两个地址,这两个地址是test 函数的地址。 那我们的函数的地址要想保存起来使用的话,怎么保存?这时候就需要函数指针保存函数地址了。
定义函数指针
观察下面哪个有能力保存函数的地址。
void (*pfun1)();
void *pfun2();
这里pfun1
先与*
结合,所以pfun1
是一个指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
。
pfun2
先与()
结合,表明这是一个函数,返回值为void*
,又因为它没有函数体,所以它是函数的声明。
使用函数指针
使用函数指针的时候,虽然可以写成(*p)()
,但是也同样等价于p()
,并且为了方便使用,硬性规定函数指针使用方法为p()
。
有趣代码加深理解
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
先说明代码1:
*
,说明这是指针,但却没有定义变量,说明是个类型。()
表明这是个函数指针类型。0
进行强制类型转换,说明0
是一个函数指针。0
,表示通过函数指针调用函数,只是这里认为函数的地址为0
。代码2:
signal
先与()
结合,说明signal
是一个函数,第一个参数为int
,第二个参数是一个函数指针。int
,返回值为void
的函数指针。代码2太复杂,可以进行简化:
typedef void(*pfun_t)(int);//函数指针类型重命名为pfun_t
pfun_t signal(int, pfun_t);//函数声明
函数指针数组的定义
例:
int(*parr[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 div1(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 = div1(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
使用函数指针数组的实现:
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div1 }; //转移表
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);
printf("ret = %d\n", ret);
}
else
printf("输入有误\n");
}
system("pause");
return 0;
}
指向函数指针数组的指针
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针ptest
void(*ptest)(const char*) = test;
//函数指针的数组ptestarr
void(*ptestarr[10])(const char*) = { ptest };
//指向函数指针数组首元素的指针pptest
void(*(*pptest))(const char*) = ptestarr;
//指向整个函数指针数组的指针pptest1
void(*(*pptest1)[10])(const char*) = &ptestarr;
return 0;
}
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个函数通过传过来的指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
通过qsort讲解回调函数的使用
库函数qsort
就是回调函数,看一下它的函数声明:
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));
base
是无类型的指针,可以接受任何的指针类型,但是void*
不能解引用,需要根据传入的内容强转成其他的类型进行操作。num
是传入数组的元素个数。其类型是无符号整型。size
是传入数组的元素大小(字节个数)。compar
就是函数指针,用于调用其他函数。代码使用qsort
#include
#include
#include
//回调函数比较整型调用的函数
int CompInt(const void* xp, const void* yp)
{
assert(xp);
assert(yp);
return (*(int*)xp - *(int*)yp);
}
int main(void)
{
int arr1[] = { 123,74,724,654,85,946,24325,4363574,-23,-534,24,235,43 };
int n1 = sizeof(arr1) / sizeof(arr1[0]);
qsort(arr1, n1, sizeof(arr1[0]), CompInt);
for (int i = 0;i < n1;i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
return 0;
}
运行结果:
用冒泡排序实现回调函数qsort
#include
#include
#include
#include
//按字节交换位置
void Swap(char* xp, char* yp, size_t size)
{
while (size--)
{
char temp = 0;
temp = *xp;
*xp = *yp;
*yp = temp;
++xp;
++yp;
}
}
//回调函数比较整型调用的函数
int CompInt(const void* xp, const void* yp)
{
assert(xp);
assert(yp);
return (*(int*)xp - *(int*)yp);
}
//回调函数比较浮点型调用的函数
int CompDouble(const void* xp, const void* yp)
{
assert(xp);
assert(yp);
const double* x = (const double*)xp;
const double* y = (const double*)yp;
if (*x - *y > 0)
{
return 1;
}
else if (*x - *y < 0)
{
return -1;
}
else
{
return 0;
}
}
//回调函数比较字符串调用的函数
int CompString(const void* xp, const void* yp)
{
assert(xp);
assert(yp);
//用char*保存字符串时
const char* x = *(const char**)xp;
const char* y = *(const char**)yp;
while (*x || *y)
{
if (*x > * y)
{
return 1;
}
else
{
return -1;
}
x++;
y++;
}
return 0;
return strcmp(x, y);
}
//模仿qsort的功能实现一个通用的冒泡排序
void MyQsort(void* a, size_t n, size_t size, int(*comp)(const void* xp, const void* yp))
{
assert(a);
assert(comp);
char* p = (char*)a;
int s = 1;
for (size_t i = 0;i < n - 1; i++)
{
for (size_t j = 0;j < n - i - 1;j++)
{
if (comp(p + j * size, p + (j + 1) * size) > 0)//这里是字节操作
{
Swap(p + j * size, p + (j + 1) * size, size);
s = 0;
}
}
if (s)
break;
}
}
int main(void)
{
int arr1[] = { 123,74,724,654,85,946,24325,4363574,-23,-534,24,235,43 };
int n1 = sizeof(arr1) / sizeof(arr1[0]);
//qsort(arr1, n1, sizeof(arr1[0]), CompInt);
MyQsort(arr1, n1, sizeof(arr1[0]), CompInt);
for (int i = 0;i < n1;i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
double arr2[] = { 12.3,7.4,72.4,6.54,0.85,94.6,24.325,4363.574,-2.3,-53.4,24.567,235.555,4.3 };
int n2 = sizeof(arr2) / sizeof(arr2[0]);
//qsort(arr2, n2, sizeof(arr2[0]), CompDouble);
MyQsort(arr2, n2, sizeof(arr2[0]), CompDouble);
for (int i = 0;i < n2;i++)
{
printf("%.3f ", arr2[i]);
}
printf("\n");
char* arr3[] = {
"fxakssxb!",
"aadcasvaf",
"acvsvffd=",
"dasn2r32",
"3254376fsda",
"543654bsggs"
};
int n3 = sizeof(arr3) / sizeof(arr3[0]);
//qsort(arr3, n3, sizeof(arr3[0]), CompString);
MyQsort(arr3, n3, sizeof(arr3[0]), CompString);
for (int i = 0;i < n3;i++)
{
printf("%s\n", arr3[i]);
}
printf("\n");
system("pause");
return 0;
}
运行结果:
字符串的大小比较是从第一个元素开始往后依次比较字符的ascii码值大进行排序,只要其中有一个字符大,那么这个字符串就比另一个大。
笔试题1
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//程序的结果是什么?
结果为:
2 ,5
结果分析:
&a
的类型是int[5] *
,加一后指向整个数组后跟数组大小相同的空间的首字节地址,并把地址传给了ptr
。由于ptr
是int
型,只向前移动四个字节,所以指向数组的最后的元素。
笔试题2
//结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
结果为:
100014
100001
100004
结果分析:
p
的类型是struct Test*
,指针加一加上其所指向类型的大小,故加上结构体的大小20,转换为地址的16进制输出即为结果100014
。- 把
p
的类型强转为unsigned long
,加一即为数字之间的加法,故直接加上一,结果为100001
。- 把
p
的类型强转为unsigned int*
,指针加一加上其所指向类型的大小,故加上int的大小4,所以结果为100004
。
笔试题3
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
结果为:
4,2000000
结果分析:
内存结构图:
&a
的类型是int[4] *
,加一后指向整个数组后跟数组大小相同的空间的首字节地址,并把地址传给了ptr
。由于ptr
是int
型,只向前移动四个字节,所以指向数组的最后的元素。- 将
a
强转为int
类型加一就是值加一,所以就相当于地址值加了一,再强转为int*
类型传给ptr2
解引用后,小端取出的数值为02 00 00 00
,若为大端,取出的数值为00 00 01 00
。
笔试题4
#include
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);
return 0;
}
结果为:
1
结果分析:
a
数组的元素是类型为int[2]
的一维数组,p
保存的是a
首元素的地址,p[0]
访问的是a[0][0]
。通过逗号表达式可知,数组初始化的内容是a[0][0] = 1,a[0][1] = 3,a[1][0] = 5
,所以打印出的结果是1。
笔试题5
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
结果为:
ff ff ff fc,-4
结果分析:
a
数组的内部元素是int[5]
,即有5个int
型的一维数组,一共有5 * 5 = 25个int
型的数据。p
指针的类型是int[4] *
,p
指针保存了数组的首字节地址。元素p[4][2]
前面有4 * 4 + 2 = 18个int
的数据,元素a[4][2]
前面有4 * 5 + 2 = 22个int
的数据。&p[4][2] - &a[4][2]
相减时,是两个相同的指针类型相减,等于跨越的数据个数为18 - 22 = -4。-4
以%p
打印出来就是-4的补码ff ff ff fc
,以%d
打印出来就是-4
。
笔试题6
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
结果为:
10,5
结果分析:
&aa
的类型是int[2][5] *
,加一加上其指向类型的大小,所以ptr1
指向整个数组后跟数组大小相同的空间的首字节地址,由于ptr1
的类型是int
,所以往前移动了4个字节,解引用后就是10。- 数组
aa
的元素类型是int[5]
,即具有5个int
数据的一维数组。加一指向后一个元素,把该地址传给ptr2
,由于ptr2
是int
型,往前移动四个字节解引用就是5。
笔试题7
#include
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
结果为:
at
结果分析:
数组
a
元素类型是char*
,保存的是三个字符串的首地址。pa
指向数组首元素,pa++
后指向数组的第二个元素,解引用后打印出来就是字符串at
。
笔试题8
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}
结果为:
POINT
ER
ST
EW
结果分析:
- 数组
c
中保存的是三个字符串的首元素地址,数组中cp
保存的是数组c
元素的地址,注意初始化的时候重新赋了值,cpp
中保存了cp
的首元素地址。++cpp
,改变了cpp
里保存的地址,指向了cp[1]
,cp[1]
指向c[2]
,解引用的时候就打印了"POINT"字符串。++cpp
,又改变了cpp
里保存的地址,指向了cp[2]
,*++cpp
访问cp[2]
,--*++cpp
使cp[2]
指向c[0]
,*--*++cpp
访问c[0]
空间,c[0]
空间里保存的"ENTER"字符串首元素地址+3后打印就是"ER"。cpp[-2]
访问了cp[0]
,*cpp[-2]
通过cp[0]
里保存的地址访问c[3]
,c[3]
空间里保存的"FIRST"字符串首元素地址+3后打印就是"ST"。cpp[-1]
访问cp[1]
,cpp[-1][-1]
通过cp[1]
保存的地址访问c[1]
,c[1]
空间里保存的"NEW"字符串首元素地址+1后打印就是"NW"。