本章重点:
- 字符指针
- 数组指针
- 指针数组
- 数组传参和指针传参
- 函数指针
- 函数指针数组
- 指向函数指针数组的指针
- 回调函数
我们在指针的初阶的时候主要讲了:
1.指针就是变量,用来存放地址,地址唯一标识一块内存空间
2.指针的大小是固定4个字节/8个字节(32为平台/64位平台)
3.指针是有类型的,指针的类型决定了指针+-整数的步长,指针解引用操作的时候的权限。
4.指针的运算
这里我们来探讨指针更高级的主题
1.字符指针
字符指针就是char*类型的指针
一般使用:
使用1:
使用2:
一般表达式都有两个属性,值属性和类型属性。
char* p="abcdef",是把字符串首元素的地址放到p中。用%s打印时,只需要提供一个起始地址即可。这里"abcdef"是一个常量字符串,不能被该,所以我们要加上const修饰
cs
#include<stdio.h>
//字符指针
int main()
{
const char* p = "abcdef";//把字符串首元素的地址放到p中
printf("%s", p);
return 0;
}
要是想把字符串放进一个变量里面,需要创建一个数组
练习:分析下列代码及其结果产生的原因
首先p1,p2中存放的都是常量字符串,常量字符串不能被改,没必要存放多份,在只读数据区,相同的常量字符串只需要存一个,而p1,p2都是首元素a的地址,指向的是同一块内存空间,所以p1==p2。而使用相同的常量字符串来初始化数组时会开辟出不同的内存块,arr1和arr2是数组名,数组名表示首元素的地址,内存空间不同,所以首元素的地址不同。
2.指针数组
指针数组是一个存放指针的数组
int arr[10]是整型数组--用来存放整形数据的数组
char arr[10]是字符数组--用来存放字符型数据的数组
int *arr[10]是整形指针数组--用来存放整型指针类型数据的数组
char *arr[10]是字符型指针数组--用来存放字符型指针类型数据的数组
我们可以用一个指针数组来模拟一个二维数组(初阶讲过)
cs
#include<stdio.h>
//指针数组
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[] = { arr1,arr2,arr3 };//数组名表示首元素地址
int i = 0;
for (i = 0;i < 3;i++)
{
int j = 0;
for (j = 0;j < 5;j++)
{
printf("%d ",*(parr[i]+j));
//printf("%d ",parr[i][j]);
}
printf("\n");
}
return 0;
}
打印方法1:
parr[i]是找到下标为i的元素(整形指针类型)的地址,parr[0]-->arr1,就是arr1这个数组的首元素的地址,parr[1]-->arr2;parr[2]-->arr3;然后让你后我们要求第几个元素就再让这个地址加上几,求arr1[0]的地址就是arr1+0,arr1[1]的地址就是arr1+1,(指针与指针相减跳过的是元素的个数,所以指针+元素的个数,得到的就是下一个指针),我们在对地址进行解引用就可以得到再arr[i][j]处元素,从而模拟出一个二维数组。
打印方法2:
有两种理解
arr[i]-->*(p+i)-->*(arr+i)-->i[arr]
所以*(arr[i]+j)-->arr[i][j]
还可以理解成arr[i]-->arr1,arr2,arr3,而访问元素通过下标来访问,就是arr1[0],访问数组1的第一个元素。
3.数组指针
([]的优先级高于*,所以要加上括号表示先结合)
整型指针--指向整形的指针int a=3; int* p=&a;指针变量p是int*类型的,指向的是元素是a,a是一个整形类型的数据,所以p是整形指针
数组指针--指向数组的指针int(*p)[10],p是一个数组指针,p可以指向一个数组,该数组有10个元素,每个元素是int类型的(指向一个有10个元素的整型数组)。
这里p的类型是int(*)[10](数组指针类型),存放的是arr这个数组所有元素的地址(之前在数组章节说过arr表示的首元素地址,两种情况除外1.sizeof 2.&arr)
再举个例子:
有个指针数组,char* arr[5]={0};
若要将这个指针数组整个数组的地址存放起来,需要用什么接收
char *(*p)[5],需要用一个数组指针接收,这个数组指针指向的内容是这个指针数组就是char*arr[5],这个数组指针的类型是char*(*)[5]。
在定义类型时int(*)[ ] ,一定要把[ ]里面的数加上,指明指向的数组有几个元素。
数组指针一般用在二维数组中。
对于一维数组:
我们如果想访问它的元素,或者通过他的地址改变它的元素,只需要存入首元素的地址,用一个指针变量存放即可,用数组指针存放整个数组的指针还会将问题复杂化。
cs
#include<stdio.h>
//数组指针
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = arr;
int i = 0;
for (i = 0;i < 10;i++)
{
printf("%d ", *(p + i));
}
return 0;
}
cs
#include<stdio.h>
//数组指针
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int (*p)[10] = &arr;
int i = 0;
for (i = 0;i < 10;i++)
{
printf("%d ", *(*p + i));//解引用p就相当于通过p找到了arr这个数组,而arr又是数组名,数组名就是首元素的地址,再通过他在到底i个元素的地址,在解引用,才能访问到数组内容
}
//int a = 0;
//int*p=&a
return 0;
}
对于二维数组
cs
#include<stdio.h>
//数组指针
void print_(int arr[][4], int r, int c)
{
int i = 0;
for (i = 0;i < r;i++)
{
int j = 0;
for (j = 0;j < c;j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
print_(arr, 3, 4);
return 0;
}
根据下边实际传入地址我们知道列是不可以省略的。需要将第一行的元素都传入函数。
我们也可以用数组指针表示,二维数组的数组名表示的也是首元素的地址,但是二维数组的首元素是他第一行的元素。我们接受这个二维数组,就应该用一个数组指针接受。
cs
#include<stdio.h>
//数组指针
void print_(int (*p)[4], int r, int c)
{
int i = 0;
for (i = 0;i < r;i++)
{
int j = 0;
for (j = 0;j < c;j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
print_(arr, 3, 4);
return 0;
}
*(*(p + i) + j)的理解:存的是一行的元素的地址,(p+i)就相当于第i行的元素的地址(虽然数据是连续存储的,但是我们可以把发看成一个三行四列的)*(p+i)就是通过这个地址找到了第一行的元素,(而二维数组可以看成一维数组的数组,就是可以将指向的第一行看成arr[0]此时arr[0]是一个数组名,数组名又是这行元素的首元素的地址,所以,可以通过,首元素的地址+j,找到第j列的元素的地址,在对这个地址进行解引用,就得到了第i行,第j列元素的地址。
int(*p)[4];p的类型是:int(*)[4],p是指向一个整型数组的,数组5个元素 int[5],p+1,表示跳过5个int元素的数组。
数组指针和指针数组:
指针数组就是一个数组中放的元素都是由地址组成的数组,可以是&a,&b,&c,将几个元素的地址放在指针数组中,也可以是将几个数组的首元素的地址放在指针数组中,例如:模拟二维数组(我们就可以通过这个每个数组的首元素找到这个数组,在由数组名找到每一个元素,对他进行访问。
而数组指针是指向一个数组的指针,存放的是这个数组整个数组的地址,但我们一般不单于存放一个数组的地址,我们通常使用的是存放一个二维数组(数组名表示第一行元素的地址,将第一行元素传过去由一个数组指针接收,然后通过这个数组指针访问每一行的元素)
我们来分析下面代码的意思
int arr[5];
int *parr1[10];
int(*parr2)[5];
int(*parr3[5])[3];
arr是一个数组,存放5个整型元素parr1是一个指针数组,存放指针变量的数组
parr2是一个数组指针,是一个指针,指向一个int [5]有五个元素组成的整型数组
parr3是一个存放数组指针的数组,首先在()里面parr3先和[]结合,构成一个数组,然后这个数组的类型是int(*)[3]是一个数组指针类型,表示一个数组里有5个元素,每个元素指向的都是一个有三个元素的数组。
4.数组参数和指针参数
一维数组传参:
void test1(int arr1[])//数组传参由数组接受,元素个数可以不写,传入的是首元素的地址
{ }
void test1(int arr1[10])
{ }
void test1(int *arr)//数组传参实际上传入的是首元素的地址,可以有指针变量接收
{ }
void test2(int*arr[])//指针数组由数组接受
{ }
void test2(int **arr)//指针数组是一个存放指针的数组,相当于存放的元素是指针变量,指针变量的地址应该由二级指针接收(二级指针是一个存放一级指针的指针)
{ }
int main()
{
int arr1[10] = { 0 };
int* arr2[20] = { 0 };
test1(arr1);
test2(arr2);
return 0;
}
二维数组的传参:
void test(int arr[3][4])//二维数组传参由一个二维数组接收
{ }
void test(int arr[][4])//行可以省略,列不能省略,传入的是第一行元素的地址,需要知道第一行有多少个元素
{ }
void test(int(*p)[4])//二维数组传参,实际上传的是第一行的地址,应该用一个数组指针接收一行的地址,还需要知道一行有多少个元素
{ }
int main()
{
int arr[3][4];
test(arr);
return 0;
}
一级指针传参:
void test(int *p)//一级指针传参由一级指针接收
{ }
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = arr;
test(p);
return 0;
}
如果函数的形式参数部分是一级指针:传入的可能是一级指针变量,可能是某个元素的地址(地址由指针接收),也可能是数组名(数组名就表示首元素的地址),不过还要注意一级指针的类型
二级指针传参:
void test(int** p)
{ }
int main()
{
int n = 0;
int* p = &n;
int** pp = &p;
test(pp);//二级指针传参由二级指针接收
test(&p);//一级指针的地址传参有二级指针接收
return 0;
}
如果函数的形式参数是二级指针,传入的可能是二级指针变量,也可能是一级指针的地址,也可能是指针数组(指针数组是一个存放指针的数组,相当于存放的元素是指针变量,指针变量的地址应该由二级指针接收(二级指针是一个存放一级指针的指针))。
5.函数指针
指向函数的指针就是函数指针
函数在创建时就有地址,和全局变量一样,函数在代码区,代码区是只读的,不能被修改。
&数组名,是取出数组的地址,同理&函数名是取出函数的地址,但是对于函数来说&函数名和函数名都是取出函数的地址(对于函数没有首元素一说)
定义一个函数指针有两种写法:
int ADD(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &ADD;
int (*pf)(int, int) = ADD;
return 0;
}
这里(*pf)表示pf是一个指针,指向的是(int,int)是一个函数,函数的返回值类型为int,所以pf是一个函数指针,指针类型是int(*)(int,int)。
若果我们定义一个整形指针的话,我们可以解引用这个指针变量,访问它指向的元素,并可以对这个元素进行修改,那我们也可以通过解引用访问这个函数,从而进行传参就是(*pf)(x,y)。这里我们也可以不解引用,访问函数就行传参,再定义函数指针的第二种写法将函数名赋给指针变量pf那么pf就相当于这个函数名,我们一般的函数调用是ADD(x,y),直接调用函数传参,那么同理,我们也可以写成pf(x,y)。但需要注意加*号的时候一定要带上括号,否则会先调用函数(就是利用第二种调用方法,调用过后在对这个值进行解引用,此时这个值是函数返回的一个int类型的数不能对他进行解引用)
所以利用函数指针调用函数也有两种写法:
#include<stdio.h>
//函数指针
int ADD(int x, int y)
{
return x + y;
}
int main()
{
//int (*pf)(int, int) = &ADD;
int (*pf)(int, int) = ADD;
int ret1 = pf(3, 4);
int ret2 = (*pf)(3,4);
printf("%d\n", ret1);
printf("%d\n", ret2);
return 0;
}
函数指针的初步应用:
可以在将这个函数以函数指针的形式传递给另一个函数,在另一个函数使用这个函数时,就不需要再调用这个函数了。
cs
#include<stdio.h>
//函数指针的应用
int ADD(int x, int y)
{
return x + y;
}
void calc(int(*p)(int, int))
{
int a = 3;
int b = 4;
int ret = p(3, 4);
printf("%d\n", ret);
}
int main()
{
calc(ADD);
return 0;
}
观察两段有趣的代码(来自于C陷阱和缺陷)
(*(void(*)())0)();
//void(*)(),是函数指针类型,(void(*)())0,是将int型的0,强制类型转换为函数指针类型,这个代码是一次函数调用,调用的是0作为地址处的函数,首先把0强制类型转换为:无参,返回值类型为void的函数的地址。在调用0地址处的这个函数。
void(*signal(int, void(*)(int)))(int);
//这是一个函数指针类型的函数声明,signal函数的的第一个参数类型为int,第二个参数的类型为 void(*)(int)函数指针类型,函数指针类型的函数声明的函数指针类型是一个指向函数的参数是int,指向函数的返回值类型void的函数指针。signal函数的返回值类型没写,也默认是void。
这里我们可以知道*p(int,int)是一个函数声明,先于函数结合,p函数返回值类型为void,(*p)(int,int)是一个函数指针,指向的函数的参数类型为int ,int型,指向函数的返回值类型也是void。()的优先级大于*。
我们将第二段代码简化一下:
//typedef unsigned int uint;//把unsigned int,定义为uint
typedef void(*pf_t)(int);//把void(*)(int)类型重命名为pf_t
int main()
{
//void(*signal(int, void(*)(int)))(int);
pf_t signal(int, pf_t);
return 0;
}
进一步应用函数指针:实现简单加减乘除的计算器
初写代码
cs
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//函数指针的应用
void menu()
{
printf("******************************************\n");
printf("*********** 1.add 2.sub ***********\n");
printf("*********** 3.mul 4.div ***********\n");
printf("*********** 0.exit ***********\n");
printf("******************************************\n");
}
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 div(int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请输入你的选择->");
scanf("%d", &input);
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
switch (input)
{
case 1:
printf("%d\n", add(x, y));
break;
case 2:
printf("%d\n", sub(x, y));
break;
case 3:
printf("%d\n", mul(x, y));
break;
case 4:
printf("%d\n", div(x, y));
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误请重新输入\n");
break;
}
} while (input);
return 0;
}
这个代码我们可以实现简单的加减乘删除,但如果我们输入的0,或者输入错误的时候他不会直接提醒我们输入错误,而是会继续让我们输入两个整数,这个时候我们就应该考虑一下如何改进这个代码,初步改进:我们可以将输入两个整数的算法放到计算器内部
cs
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//函数指针的应用
//模拟简单计算器
void menu()
{
printf("******************************************\n");
printf("*********** 1.add 2.sub ***********\n");
printf("*********** 3.mul 4.div ***********\n");
printf("*********** 0.exit ***********\n");
printf("******************************************\n");
}
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 div(int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
switch (input)
{
case 1:
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
printf("%d\n", add(x, y));
break;
case 2:
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
printf("%d\n", sub(x, y));
break;
case 3:
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
printf("%d\n", mul(x, y));
break;
case 4:
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
printf("%d\n", div(x, y));
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误请重新输入\n");
break;
}
} while (input);
return 0;
}
但是这样写,显而易见有很多重复的步骤,所以我们可以进一步改进:用一个函数来分装这一个过程,在这里面调用加法器,减法器......,根据传入函数的不同,我们可以在一个函数里面进行不同的运算。要是直接在函数内部调用add,一次只能调用一个函数,本质上还是需要重复这个步骤,但是我么可以由函数指针接受不同的函数完成每一步的调用,大大减少了代码重复率。
cs
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//函数指针的应用
//模拟简单计算器
void menu()
{
printf("******************************************\n");
printf("*********** 1.add 2.sub ***********\n");
printf("*********** 3.mul 4.div ***********\n");
printf("*********** 0.exit ***********\n");
printf("******************************************\n");
}
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 div(int x, int y)
{
return x / y;
}
int calc(int(*p)(int,int))
{
int x = 0;
int y = 0;
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
int ret = p(x, y);
printf("%d\n",ret);
}
int main()
{
int input = 0;
do
{
menu();
printf("请输入你的选择->");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误请重新输入\n");
break;
}
} while (input);
return 0;
}
这样我们就大大简化了计算机设计的步骤。
6.函数指针数组
把函数的指针存到一个数组中,那这个数组就叫函数指针数组。
函数指针数组的定义:
//函数指针数组
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 div(int x, int y)
{
return x / y;
}
int main()
{
int(*pf)(int, int) = add;
int(*arr[4])(int, int) = { add,sub,mul,div };
}
函数指针数组的调用:int main()
{
int(*pf)(int, int) = add;
int(*arr[4])(int, int) = { add,sub,mul,div };
int i = 0;
for (i = 0;i < 4;i++)
{
int ret = arr[i](3, 4);//调用函数指针时可以解引用也可以不解引用
printf("%d\n", ret);
}
}
由函数指针数组,我们还可以进一步把上面模拟简单计算器的实现,用函数指针数组的思想进行调用
cs
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
函数指针数组的应用
模拟简单计算器
void menu()
{
printf("******************************************\n");
printf("*********** 1.add 2.sub ***********\n");
printf("*********** 3.mul 4.div ***********\n");
printf("*********** 0.exit ***********\n");
printf("******************************************\n");
}
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 div(int x, int y)
{
return x / y;
}
int(*arr[4])(int, int) = { add,sub,mul,div };
int calc()
{
int(*arr[])(int, int) = { 0,add,sub,mul,div };
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int(*arr[])(int, int) = { 0,add,sub,mul,div };
do
{
menu();
printf("请输入你的选择->");
scanf("%d", &input);
if (input == 0)
{
printf("退出游戏");
}
else if (input >= 1 && input <= 4)
{
printf("请输入两个整数->");
scanf("%d %d", &x, &y);
int ret = arr[input](x, y);//通过数组下标访问,找到这个函数
printf("%d\n", ret);
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
这种方法简化了修改代码,要是想要加别的计算,直接放到数组里面,在改变一下条件的范围即可。在数组首位补0,可以领输入的数直接跳转到需要的函数的位置,进而对函数进行调用。
7.指向函数指针数组的指针
int(*(*p)[10])();;
//首先p先和*结合是一个指针变量,然后这个指针指向一个数组,所以是数组指针,这个数组有是函数指针类型int(*)()的数组,所以这是一个指向函数指针数组的指针。
8.回调函数
回调函数就是一个通过函数指针调用的函数,若果你把函数指针或者地址作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数(上面我们用函数指针和函数指针数组模拟简单计算器时都用到了回调函数)回调函数不是由该函数的实现方直接调用的,而是在特定的时间或者条件发生时由另外一方调用的,用于对该事件或者条件进行响应。