指针的难点在于它不仅仅只有一种指针类型,每一种类型结构不同,根据不同的指针类型有:字符指针、数组指针、指针数组、函数指针和函数指针数组,这几个指针的难点在于不同指针类型意义不同以及结构的记忆容易混乱。那么本篇文章呢以代码结合的方式解释他们的结构,以及组成部分的含义和怎么理解和记忆不同的指针类型。
首先我们先介绍什么是指针,得先知道他的概念:
1.指针就是个变量,用来 存放地址,地址唯一标识一块内存空间。
2.指针的大小在不同平台下的大小也不同,但他是固定的,常见的32位和64位平台下指针的大小分别为4byte/8byte,如下图:
3.指针是有类型的,指针的类新决定了指针+-整数的步长,解引用操作的时候的权限。
当我们加减1时再看看指针一步的步长是多少?(64位平台),如下图:
我们先介绍字符指针:
char ch = 'q' ; char *pc = &ch;
首先创建一个字符变量ch,然后取出ch的地址存放到指针pc里,这里的*表示pc是一个指针,*前面的char表示这个指针pc指向指向变量ch是char类型。
然后我们来看字符指针,指针即地址,且关键字是字符。既然他是一个指针那么指针变量里自然放的是字符的地址
当我们字符指针存放字符串时字符指针的大小肯定不够存放整个字符串的大小那么字符指针存的是什么呢?,即:
通过上面代码我们可以看见字符指针存的是字符串首元素的地址!
但如果是字符数组 char arr [ ] = "abcde" ;存的是整个字符串的大小。
下面我们再看一幅图
为什么str1和str2不同,str3和str4相同?
指针指向相同的一个空间,数组则分别开辟了两个空间(1.指针就是个变量,用来 存放地址,地址唯一标识一块内存空间。)
并且str3和str4的位置为常量字符串他们是不能被改的 为避免误解尽量加上const char *str3 / const char *str4 。
指针数组
首先我们要考虑指针数组他是指针呢还是数组呢?然后从这里出发去理解,指针数组他是一个数组,相信大家已经学整形数组,字符数组.......等以及二维数组,我们来通过学的这些来对比,整形数组 int arr [10] = {1,2,3,4,5,6,7,8,9,10};看见中括号我们知道arr是一个数组,数组元素有10个,元素类型是整形。数组名前面是整形int就说明数组元素类型是整形。我们在看看字符数组。即:char arr[10] = {'a','b','c','d'}; 同样的见中括号我们直到arr是一个数组,数组元素有10个,元素类型是字符型。数组名前面是char型就说明数组元素类型是字符型。看到这里我们在看看指针数组,指针数组就是一个存放指针的数组 。他的构成是 int *arr[ ] = { 存放的是整形指针的地址};,我们通过前两个整型数组和字符数组对比发现,指针数组他依然是一个数组,数组的前面是整形指针,那么数组里存的就是整形指针,即整形的地址。
现在解释上面那个代码,首先我们创建三个数组a , b , c 然后创建一个指针数组arr存放三个数组a,b,c的地址(数组名即首元素地址),因为三个数组存放的是整形所以指针数组arr里存放的是整形指针int*,如果是字符那么就是char* , 这个时候可以将三个数组看成一个二维数组,arr数组名为首元素地址而二维数组的首元素是一个数组,所以当arr[i]中i是0的时候打印a数组的元素以此类推依次打印b和c数组。然后利用for循环依次打印下一个元素,i控制的是数组下标,j控制的是数组里面元素的下标。
数组指针
同样的我们得先知道数组指针是指针呢还是数组呢?数组指针是一种指针,指向数组的指针,这里大家可能不理解,我们通过对比看看数组指针到底指向的是什么。
在前面我们已经学习过普通的指针,比如:整形指针int a = 20; int *p = &a; 和 字符指针char ch = 'w' ;char*p = &ch; &a存的是整型变量a的地址,存到指针变量p里面 *说明p是一个指针,既然&a存的是整型变量a的地址那么指针类型自然也是int整形,字符指针自然也是同理的了。
再看数组指针,数组指针同样是一种指针,比如整形数组指针:
首先&a的地址然后存进arr中 *号说明arr是一个指针,如果不加()*号先和[ ]结合那么就是指针数组了,加上()后 *和arr结合说明arr是一个指针,指向数组,数组元素个数为10个,元素类型为int整形。既然是数组指针,他是一个指针,指针存的是地址,通过整形指针和字符指针对比那么数组指针指向数组的地址。
在这里我们还得分清arr与&arr的区别,看下面代码:
无论是arr还是&arr都是同样的地址可见无论是arr还是&arr它们的首地址相同,我们在验证一下arr和&arr都加1后的结果、
8到c相差四个,十六进制转化成十进制后等于4,B8到E0相差2和8,十六进制转化成十进制数后等于40,所以arr和&arr的区别在这里。
二数组名是首元素的地址有两个例外
1.sizeof(数组名) - 数组名表示整个数组,计算的是整个数组大小,单位是字节
2.&数组名 - 数组名表示整个数组,取出的是整个数组的地址
数组指针的使用
分析:该代码传参传的是二维数组名,数组名是首元素地址,二维数组的首元素为一维数组,即{1,2,3,4,5} 那么数组指针p解引用一次为一维数组的首元素的地址再解引用一次得到一维数组的首元素,然后呢形参接受的是一个一维数组的地址所以采用数组指针接受。
函数指针
首先函数指针它是一个指针,指针及地址那么函数指针它指向的是什么呢?通过上面的学习我们发现数组指针指向的是数组的地址,对比可知函数指针就是指向的函数的地址了,知道这个后呢我们还要知道函数的地址怎么表示呢以及函数指针的结构是什么样的,下面我们先讨论函数的地址。
我们知道数组名和&数组名分别表示数组首元素的地址和整个数组的地址,那么函数名和&函数名呢?如图:
无论是Add还是&Add它们的地址是一样的,那么他们有区别吗?首先如果是数组名和&数组名他们的区别什么介绍了,那么函数名和&函数名是否有区别呢?我们想想数组是有首元素的函数名有首元素吗?当然没有,那么我们就可以得到以下结论:
知道这个结论后我们在看看函数指针的构成方式,如下图:
然后结构中的pf就是一个函数指针变量。下面我们讨论函数指针的使用
上面我们说函数名和&函数名等价,即两个都表示的是函数的地址,上面的代码还可以写成
然后呢Add也表示函数Add的地址,同时函数指针变量pf也是函数Add的地址那么我们可以认为
Add ==pf , 二者也等价。我们通过函数名调用:
既然Add也可以表示函数的地址,pf和Add等价,那我们通过pf调用
然后我们对比第一个ret去掉*号后仍然能调用函数,那么*号只是一个摆设无论括号里有多个*号他依然能成果调用。如下图:
在看看以下代码
void(*signal(int, void(*)(int)))(int); 这种形式我们转化成void(*)(int) signal(int, void(*))(int);
解释下面注释的代码:signal的地址函数参数是整形int, 函数指针类型void(*)(int),返回类型为void(*)(int) , 但是语法不支持这么写。这样拆开需要用到typedef-对类型进行重定义。
函数指针数组
整型数组、字符数组、指针数组都是数组,它们各自元素分别是整形、字符型、指针,那么函数指针数组存的就是函数指针的数组,即函数的地址。
当我们知道了函数的地址后,用数组存的话就需要使用函数指针数组,那么函数指针数组的构成是什么样呢,如下图:
pfarr先和[ ]结合构成数组,把pfarr[2]去掉剩下的就是数组存的类型,即int (*)(int, int)-函数指针类型,函数指针指向的是函数的地址,那么函数指针数组存的函数指针也就是存的函数的地址,后面花括号内分别是函数Add和函数Sub的地址。
下面我们以计算器为例:
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;
}
void menu()
{
printf("*****************************\n");
printf("*****1.add 2.sub*****\n");
printf("*****3.mul 4.div*****\n");
printf("********** 0.exit **********\n");
printf("*****************************\n");
}
int main()
{
int input = 0;
do
{
menu(); //菜单
int x = 0;
int y = 0;
int ret = 0;
// 选择运算方式
printf("请选择:\n");
scanf("%d", &input);
switch (input)
{
case 1: //加法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
int ret = Add(x,y);
printf("ret = %d\n", ret);
break;
case 2: //减法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3: //乘法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4: //除法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default :
printf("选择错误,请重新输入:\n");
}
} while (input);
return 0;
}
通过这个代码我们发现此计算机只能求加、减、乘、除,如果后续我们想添加其它运算不方便存在局限性。并且只要加一个运算case后面就重复一部分代码,如图:
case 1: //加法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
int ret = Add(x,y);
printf("ret = %d\n", ret);
break;
case 2: //减法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3: //乘法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4: //除法
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("ret = %d\n", ret);
break;
那么解决办法就是依靠我们的函数指针数组,如下图:
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;
}
void menu()
{
printf("*****************************\n");
printf("*****1.add 2.sub*****\n");
printf("*****3.mul 4.div*****\n");
printf("********** 0.exit **********\n");
printf("*****************************\n");
}
int main()
{
int input = 0;
do
{
menu(); //菜单
//函数指针数组-pfArr就是一个函数指针数组
int(*pfArr[5])(int, int) = {NULL, Add,Sub,Mul,Div }; //添加一个空指针,使函数的下标
//对应input
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个数字\n");
scanf("%d %d", &x, &y);
//使用函数指针数组访问函数
ret = (*pfArr[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出程序\n");
}
else
{
printf("选择错误,请重新选择\n");
}
} while (input);
return 0;
}
通过对比发现,使用函数指针数组将case重复使用的代码省略了,提高了效率和可读性。
回调函数
上面的代码同样可以使用回调函数来实现,那么什么是回调函数呢?如下图:
如下代码:
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;
}
//创建函数calc,参数设置成函数指针形式,设置成函数指针形式是为了接受不同函数的地址
int calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入两个数:\n");
scanf("%d %d", &x, &y);
//使用函数指针调用函数
return pf(x, y);
void menu()
{
printf("*****************************\n");
printf("*****1.add 2.sub*****\n");
printf("*****3.mul 4.div*****\n");
printf("********** 0.exit **********\n");
printf("*****************************\n");
}
int main()
{
int input = 0;
do
{
menu();
int ret = 0;
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
ret = calc(Add);
printf("ret = %d\n", ret);
break;
case 2:
ret = calc(Sub);
printf("ret = %d\n", ret);
break;
case 3:
ret = calc(Mul);
printf("ret = %d\n", ret);
break;
case 4:
ret = calc(Div);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,请重新输入:\n");
}
} while (input);
return 0;
}
在这里分析代码,首先创建加减乘除的函数,再自建函数calc,calc的参数设置成函数指针,函数指针的参数分别是int , int 返回类型是int 即:int (*pf)(int , int) 。然后将在do_while循环中的函数calc赋值给ret 同时参数分别是加减乘除函数的地址。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针即地址作为参数传递给另外一个函数,当这个指针被用来调用其所指向的函数时,我们就说这就是回调函数。