指针的进阶
本文旨在探讨指针的高阶妙用,默认大家已经对指针的基本概念已经熟练掌握。
一、字符指针
字符指针的赋值
- 先创建字符串,再赋值给指针
- 创建字符串时,就赋值给指针
代码示例
直接跟着代码来分析它们的区别吧:
int main()
{
/*方式1:先创建字符串,再赋值给指针*/
char arr[] = "abcd"; // 在内存中创建了一个字符数组,并将数组的初值赋值为a,b,c,d,\0
char* p1 = arr; // 将内存中创建的数组首地址赋值给了p1指针
*p1 = "W"; // 数组中的第一个元素‘a’,被修改为了‘w’
/*方式2:创建字符串时,就赋值给指针*/
char* p2 = "abcd"; // 在内存中创建了一个"abcd"常量字符串,并且把这个字符串的首地址赋值给了p2
*p2 = "W"; // 这句代码虽然编译可能不会报错,但是运行会崩溃,因为常量是不能被修改的
return 0;
}
代码解析
char* p1 = arr;
代码解析:
将内存中已经创建并初始化完成的字符数组arr的首地址赋值给了p1指针。
char* p2 = "abcd";
代码解析:
先在内存中创建一个常量字符串,然后把这个字符串的首地址赋值给了p2指针。
通过
*p2 = "W";
想给常量字符串赋值时,程序会崩溃(因为常量不能被修改)。所以,如果确实想使用char* p2 = "abcd";
的方式给指针赋值,建议在前面加上const修饰符,const char* p2 = "abcd";
声明这个指针指向的是一个常量。·
进阶思考
想想以下代码段的输出结果:
int main()
{
char arr1[] = "abcd";
char arr2[] = "abcd";
char* arr3 = "abcd";
char* arr4 = "abcd";
printf("%d\n", arr1 == arr2); // 输出结果为0,arr1和arr2值不相等,因为他们指向的是不认同的内存空间
printf("%d\n", arr3 == arr4); // 输出结果为1,arr3和arr4值相等,因为他们指向的都是内存中的同一个字符串常量
return 0;
}
别忘了,在arr3和arr4的前面要加上const修饰符,养成良好编程规范。
二、指针数组
语法:类型名* 数组名[数组长度]
定义:指针数组是一种特殊的数组,其中每个元素都是指针。这意味着指针数组中的每个元素都指向另一个内存位置或对象。
代码示例:例如如下代码段,可以使用指针数组作为函数参数,对一组字符串进行排序。
void sort_strings(char **str, int n) {
// 实现排序算法,如冒泡排序等
}
int main() {
char strs[] = {"lisi", "hahaha", "hehehe", "helloa", "leihoua", "lisi", "nihaoa", "wangwu", "ajax", "bureau"};
char *pstr; // 定义指针数组
for(int i = 0; i < 10; i++) {
pstr[i] = strs[i]; // 将字符串地址存入指针数组
}
sort_strings(pstr, 10); // 对字符串进行排序
// 输出排序后的字符串
for(int i = 0; i < 10; i++) {
printf("%s\n", pstr[i]);
}
return 0;
}
三、数组指针
数组指针是一个指向数组类型的指针
数组指针操作一维数组
先来看看以下代码:
int main()
{
int arr[3] = {1,2,3};
int (*p1)[3] = &arr;
int* p2 = arr;
/*使用数组指针,遍历一维数组,方式1:先解引用,再用下标取值*/
for(int i = 0; i < 3; i++)
{
printf("%d\n", (*p1)[i]);
}
/*使用数组指针,遍历一维数组,方式2:先解引用,再移动指针步长,再解引用*/
for(int i = 0; i < 3; i++)
{
printf("%d\n", *(*p1+i));
}
/*使用整型指针遍历一维数组:直接移动指针,再解引用即可*/
for(int i = 0; i < 3; i++)
{
printf("%d\n", *(p2+i)); //*(p2+i)等价于p2[i]
}
}
有没有觉得使用int (*p1)[3]
遍历一维数组生涩难用。实际上,遍历一维数组时,我们更多的是直接使用普通指针(int* p2
的方式)。数组指针在访问二维数组时,才会体现它的妙用。
数组指针操作二维数组
先明确一点,数组指针指向的是数组首元素的地址。那么一个二维数组arr[i][j]
的首元素是谁?是arr[0][0]
吗?
- 当然不是,
arr[0]
才是这个二维数组的首元素,arr[0][0]
是这个二维数组中的第个元素arr[0]
中的第一个元素。来看看以下代码:
int main()
{
int arr[2][3] = {{1,2,3},{4,5,6}};
int (*p1)[3] = arr;
/* 以下几种操作二维数组的方式都是等价的 */
printf("%d\n", arr[1][2]);
printf("%d\n", *(*(p1+1) +2));
printf("%d\n", *(p1[1]+2));
printf("%d\n", p1[1][2]);
}
-
arr[1][2]
:直接使用数组名访问数组 -
*(*(p1+1) +2)
:arr这个二维数组中存储了2个数组类型的元素-
p1+1
使指针从数组首地址偏移了一个元素的长度,也就是指向了"{4,5,6}"这个数组的首地址。 -
*(p1+1)
在上一步的基础上对指针进行解引用,拿到了"{4,5,6}"这个数组首地址的值。(是数组首地址的值,不是数组首地址里面的值) -
*(p1+1) +2
在上一步的基础上,指针偏移了一个元素的长度,也就是指向了"{4,5,6}"这个数组中的元素’6’的地址。 -
*(*(p1+1) +2)
在上一步的基础上,对指针进行解引用,拿到了真正的数据,也就是6这个数。
-
-
*(p1[1]+2)
:在上一节数组指针操作一维数组中,我们提到了*(p1+1)
与p1[1]
是等价的。所以*(p1[1]+2)
和*(*(p1+1) +2)
也是等价的。 -
p1[1][2]
:参考第三点的逻辑,*(p1[1]+2)
与p1[1][2]
也是等价的。
一维数组传参
一维数组在函数间传递的几种方式:
void test1(int arr[3]){}; // 正确写法
void test2(int arr[){}; // 正确写法,一维度数传参时,形参可以省略元素个数
void test3(int* p){}; // 正确写法,使用数组指针传递一维数组
void test4(int* p[3]){}; // 正确写法,使用数组指针传递一维的指针数组
void test5(int **p){}; // 正确写法,使用二级指针,传递一维的指针数组
int main()
{
int arr[3] = {1,2,3;
int arr2[3] = {0};
test1(arr);
test2(arr);
test3(arr);
test4(arr2);
test5(arr2);
test4(arr); // 语法正确,但是不推荐使用数组指针接收除指针数组外的其他类型一维数组
test5(arr); // 语法错误,编译会报错,二级指针不能接收除指针数组外的其他类型一维数组
}
思考:
为什么形参使用
int arr[m]
的方式接收数组时,m可以省略?为什么形参使用
int (*p)[n]
的方式接收数组时,n能省略?答:当对指向一维数组的指针进行步数长+1时,不需要知道元素个数,只要知道元素类型即可。
二维数组传参
二维数组在函数间传递的几种方式:
void test1(int arr[2][3]){};// 正确写法,行列都确定个数
void test2(int arr[][3]){}; // 正确写法,列确认元素个数,行省略元素个数
void test2(int arr[][3]){}; // 错误写法,列省略元素个数,行确认元素个数
void test3(int arr[][]){}; // 错误写法,行列都省元素个数
void test4(int *p){}; // 错误写法,使用基本类型的指针接收二维数组后,对数组进行操作时会发生不可预知的错误
void test5(int **p){}; // 错误写法,使用二级指针接收二维数组后,对数组进行操作时会发生不可预知的错误
void test6(int (*p)[3]){}; // 正确写法,使用数组指针传递二维数组
void test7(int *p){}; // 错误写法,使用数组指针传递二维数组时,需要确定数组首元素中的元素个数
int main()
{
int arr[2][3] = {{1,2,3},{4,5,6}};
test1(arr);
test2(arr);
test3(arr);
test4(arr);
test5(arr);
test6(arr);
test7(arr);
}
总结:
- 二维数组传参时,如果形参使用
int arr[m][n]
的方式接收实参,形参的行个数可以省略,列个数不能省略。 - 二维数组传参时,如果形参使用指针的方式接收实参,必须使用数组指针
int (*p)[n]
的方式,且n不能省略。
思考:
为什么形参使用
int arr[m][n]
的方式接收数组时,m可以省略,n不能省略?为什么形参使用
int (*p)[n]
的方式接收数组时,n不能省略?答:当对指向二维数组的指针进行步数长+1时,需要根据列的个数n来决定指针跨域多少个地址长度。
指针数组传参
void test4(int** p){}; // 正确写法,传递指针数组时,需要用到二级指针。
int main()
{
int* arr[10];
test1(arr);
}
五、函数指针
函数指针的创建
函数指针是一个指向函数的指针
函数指针创建语法:函数返回值类型 (*变量名)(函数参数类型) = 函数名
函数指针创建语法说明:
- 函数返回值类型:说明函数指针指向函数的返回值类型
- (*变量名):加()是为了让*与变量名相结合,说明这是一个指针类型的变量
- (函数参数类型):根据函数指针指向函数的参数列表进行填写,不同的函数有不同的参数,这里可以省略函数形参列表中的函数名,保留形参类型即可。
int Add(int a ,int b)
{
return a+b;
}
int main()
{
int a = 1;
int b = 2;
int (*p)(int,int) = Add;
printf("%p\n",(*p)(a,b));
printf("%p\n", Add); // 打印结果为函数Add的起始地址
printf("%p\n", &Add); // 在对函数名取地址时,Add与&Add是等价的,打印结果都是函数Add的起始地址
printf("%p\n", p); // 打印结果为函数Add的起始地址,因为指针P中存的是函数起始地址
}
思考:
void (*p)();
与void *p();
的区别是什么?首先明确一点,按照运算符的优先级,()>*>数据类型。
在
void (*p)();
中,(*p)会先结合,表明这是一个指针。然后(*p)()相结合,表明这个指针指向的是一个无参的函数,最后void (*p)()再结合,表明这个指针指向的无参函数的返回值是void类型。在
void *p();
中,p()会先结合,表明这是一个无参函数。然后*p()再结合,表明这个无参函数的返回值是一个指针。最后void *p()再结合,表明这个无参函数的返回值是一个int类型的指针。
函数指针的使用
函数指针的调用推荐语法:(*指针名)(实参列表);
函数指针的调用推荐语法说明:
- (*指针名):表示对函数指针解引用,拿到指针指向的值,也就是函数起始地址。
- (实参列表):表示调用函数时,需要传递的参数值。
思考:
为什么这里叫函数指针的调用推荐语法,来看看以下代码段。实际上,指针P前面加不加*号,加几个*号都不影响函数指针的调用的。个人觉得
(*p)(a,b)
的方式更容易理解。业界也有很多人用p(a,b)
的方式,要记得这也是正确语法。
int Add(int a ,int b)
{
return a+b;
}
int main()
{
int a = 1;
int b = 2;
int (*p)(int,int) = Add;
printf("%p\n", p(a,b)); // 正确语法, 输出结果为3
printf("%p\n",(*p)(a,b)); // 正确语法, 输出结果为3
printf("%p\n",(**p)(a,b)); // 正确语法, 输出结果为3
printf("%p\n",(***p)(a,b)); // 正确语法, 输出结果为3
}
函数指针经典案例
学会函数指针的创建与使用足以满足大部分使用场景,下面列举两种函数指针的进阶组合,加强对函数指针的掌握。
-
请解读
(*(void (*)())0)();
- 按照从左到右的顺序,找到最先被执行的()为
(*)
,表明这是一个指针; - 按照运算符的优先级,
(*)()
会先结合,表明这是一个函数指针; - 按照运算符的优先级,
void (*)()
会结合,表明这是一个指向返回值是void类型函数的函数指针; - 按照运算符的优先级,
(void (*)()) 0
会结合。还记得C语言怎么类型转换吗(转换类型)待转换的数据;
,例如float y = (float) 10;
。这里也是一样的,指的是将0转换称为了void (*)()
类型。也就是说这个函数指针指向了地址为0的函数。 - 按照运算符的优先级,
* (void (*)()) 0
会结合,对函数指针*解引用,拿到了里面的值,也就是函数的起始地址0。 - 按照运算符的优先级,
(*(void (*)()) 0)()
会结合。上一节中我们提到了调用函数指针指的语法为(*指针名)(实参列表)
。上一步中,我们已经拿到了函数的起始地址,最后一步,实际上是调用了起始地址0的函数,最后面的()是它的参数列表,因为是一个空参函数,所以调用时不需要传递参数。
- 按照从左到右的顺序,找到最先被执行的()为
-
请解读
void (*signal(int, void(*)(int)))(int);
-
经过上1道题的进阶,这道题应该很快能看出来
void(*)(int)
会最先组合,表示这是一个参数列表为int类型,返回值为void类型的函数指针。 -
其次
signal(int, void(*)(int))
会结合,这里我们能看出来,signal函数有2个参数类型,分别为int
和void(*)(int)
。通过这一点,我们能分析出当前正在定义一个名为signal的函数。为什么是正在定义而不是调用?想一想,如果是调用的话,参数列表里填的应该是实际的参数,而不是参数类型。 -
经过上一步分析,我们知道了当前正在定义signal(类型1,类型2)函数。函数定义就要有函数返回类型,它的返回类型是什么?这里我们发
(*signal(int, void(*)(int))
并不能很好的结合在一起,它不能被解读为“返回值是一个没有类型的指针”。 -
现在我们的思路返回步骤2,我们已经确定了
signal(int, void(*)(int))
这部分内容是正在定义一个名为signal的函数。实际上,除了signal(int, void(*)(int))
,这段代码剩余的部分,也就是void (* )(int)
就是这个函数的返回值类型,没错,signal函数的返回值类型是一个指向“参数列表为int,返回值为void类型函数"的函数指针类型。这里非常容易绕晕,但是定义一个函数的返回值类型是一个函数指针时就是这么混乱。感兴趣的兄弟可以百度一下如何定义一个函数的返回值类型是一个函数指针。// 下面列举了声明一个函数返回值为函数指针的2种类方式,一般编码更推荐宏定义的方式。 // 方式1: 直接定义函数,同时声明返回值是一个而函数指针 void (*signal(int, void(*)(int)))(int); // 方式2: 先宏定义一个函数指针类型,再定义函数 typedef void void(* pfun_t )(int); pfun_t signal(int, pfun_t);
-
六、函数指针数组
初识函数指针数组
解释:函数指针数组,指的首先是一个数组,数组中的元素都是函数指针类型
语法:函数返回值类型 (*数组名[数组长度])(函数参数类型)
案例:int (*pa[4])(int, int)
,定义了一个长度为4的数组,数组中的元素为int (*)(int, int)
类型\
代码:
int Add(int x,int y){return x+y};
int Sub(int x,int y){return x-y};
int Mul(int x,int y){return x*y};
int Sub(int x,int Mulreturn x-y};
int main()
{
// 定义函数指针数组
int (*parr[4])(int, int) = {Add, Sub, Mul, Div};
// 遍历函数指针数组,调用里面指向的各函数
for(int i = 0; i<4; i++)
{
printf("%d\n",parr[i](2,3));
}
}
如何使用函数指针数组
函数指针数组是一种特殊的数据结构,它允许我们将函数的地址存储在数组中,从而实现通过索引直接调用不同的函数,这种机制被称为转移表。
转移表的实现可以通过选择或循环语句子和函数指针数组结合使用。例如,我们可以根据用户输入的不同选项,通过选择语句选择调用函数指针数组中对应的函数。这种方式在实现计算器程序等应用中特别有用,其中不同的运算(如加、减、乘、除)可以通过函数指针数组来实现,用户界面则通过简单的输入选择来决定调用哪个函数进行计算。例如以下代码段,就使用do/while循环+if条件判断+函数指针数组实现了一个简单的计算器功能:
int Add(int x,int y){return x+y};
int Sub(int x,int y){return x-y};
int Mul(int x,int y){return x*y};
int Sub(int x,int Mulreturn x-y};
void Manue()
{
printf("*******计算器*******\n");
printf("**1. 加法 2. 减法**\n");
printf("**3. 乘法 4. 除法**\n");
printf("**0. 退出计算器 **\n");
};
int main()
{
int (*parr[5])(int, int) = {0, Add, Sub, Mul, Div}; // 定义函数指针数组,提前封装好加减乘除需要用到的不同函数
int input = 0;
int x = 0;
int y = 0;
do()
{
Manue();
printf("请输入您想要进行的运算:");
scanf("%d", &input);
if(0)
{
printf("程序退出.\n");
}else if(1 < input && 4 > input)
{
printf("请输入两个操作数,以逗号隔开:");
scanf("%d,%d", &x, &y);
int ret = (*parr)[i](x,y);
printf("运算结果为:%d", ret);
}
else
{
printf("参数错误,请重新输入.\n");
}
}while(input); // 函数指针数组的首元素是0,当用用户输入0时,会跳出循环,函数结束。
}
七、指向函数指针数组的指针
定义:指向函数指针数组的指针,指的首先是一个指针,这指针指向的是一个数组,且数组中的元素都是函数指针类型。
语法:函数返回值类型 (*(*数组名)[数组长度])(函数参数类型)
代码示例:
int Add(int a, int b)
{
reture a+b;
}
int main()
{
int (*p1)(int, int) = Add; // 将函数Add传递给函数指针p1
int (*p2[1])(int, int) = {p1}; // 将函数指针p1,传递给函数指针数组p2作为首元素
int (*(*p3)[1])(int, int) = &p2; // 将函数指针数组p2的首地址,传递给指向函数指针数组的指针p3
}
八、回调函数
回调函数就是通过一个函数指针调用的函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。例如以下代码段,现在想象这样一种场景,marin方法值负责将DateRes1()和DateRes2()函数调度给Add()函数进行处理,并不关心它们之间的业务关系,只关注最终的处理结果:
int DateRes1()
{
return 10;
}
int DateRes2()
{
return 20;
}
int Add(int (*p1)(), int (*p2)();)
{
// Add返回前可能还有其他的任务要处理.....
int numb1 = (*p1)();
int numb2 = (*p2)();
return numb1 + numb2;
}
main()
{
int ret = Add(DateRes1, DateRes1); // 函数Add什么时候调用DateRes1和DateRes2,main方法并不关注
printf("处理结果为:%d\n", ret);
}