指针进阶
- 1. 字符指针
- 2. 指针数组
- 3. 数组指针
- 4. 函数指针
- 5. 函数指针数组
- 6. 指向函数指针数组的指针
- 7. 回调函数
在这之前,我们可以回忆下指针是什么?
a.指针就是变量,是用来存放地址的
b.指针的类型决定了访问空间大小的权限
c.指针的大小在32/64位机器上是4/8字节
d.两指针相减表示两个地址在内存中间隔多少个指针类型的字节倍数
接下来我们将进一步的深入讨论指针的高级用法
1. 字符指针
字符指针就是指向字符的的指针,用char*类型表示
我们可以看看他是如何使用的:
#include<stdio.h>
int main()
{
char ch = 'a';
char* p = &ch;
*p = 'b';
printf("%c", *p);
return 0;
}
还有一种使用方法:
int main()
{
char* p = "abcdef";
*p = 'w';
printf("%c\n", p);
return 0;
}
第一个问题
可以思考一下p里面真的会赋进去“abcdef”吗?
任何表达式都有两个两个属性:
值属性、类型属性
例如以下代码:
int a = 5;
int b = a + 5;//a+5这个表达式的值是10,10就是他的值属性
//而a+5这个表达式中a是int类型,5也是int类型,int+int还是int,所以这个表达式最后的类型还是int
//这就是他的类型属性
所以char* p = “abcdef”; 实际上赋给指针变量p的是他的值属性,它的值属性是什么呢?是字符串首元素的地址,也就是a的地址;其次, 一个字符的大小是1个字节,这里的abcdef加上最后的\0,已经7个字符了,而char*类型只有4个字节的空间大小,是放不下的;所以这里实际上存入的是首元素的地址,也就是说,p里实际上存入的是a的地址
想要全部存进去需要这样写:char arr[] = “abcedf”;
第二个问题
有些编译器在编译char* p =“abcdef”; 这段代码的时候可能会报警告,不安全?
"abcdef"实际是一个常量字符串,也就意味着这个字符串本身不能被改的,而把这个字符串的起始地址放到p里面去,p的权限就变大了,因为p是没有被修饰的,所以p是敢去改这个内容的。
所以就有可能出现以下两种情况:
情况一:
//main函数里只写下这段代码
char* p = "abcdef";
某些编译器有可能报警告,但不会报错,这样去写了,照样该执行的执行,这就是她的脾气,就像你们有些人的女朋友是不?
情况二:
char* p = "abcedf";
*p = 'w';
这样写你就真的越界了,不太恰当的比喻,违反道德底线了是吧?这样做编译器运行后会直接挂掉,调试这段代码他就会告诉你写入访问冲突。所以有些编译器在情况一会报警告也是正确的,诶,就怕你敢出这事,所以警告你。
如何纠正警告问题呢?如下代码
int main()
{
//char* p = "abcdef";
const char* p = "abcdef";
printf("%c\n", p);
return 0;
}
这时就有效的保护了后面的字符串,即使想改也改不掉,不恰当的比喻,你想干违反底线的事也有法律在卡着你不是?
可以看看曾经的一道笔试题:
int main()
{
const char* p1 = "abcdef";
const char* p2 = "abcdef";
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (p1 == p2)
printf("p1==p2\n");
else
printf("p1!=p2\n");
if (arr1 == arr2)
printf("arr1 == arr2\n");
else
printf("arr1 != arr2\n");
return 0;
}
这里的运行的结果是什么呢?
p1 == p2 arr1 != arr2
为什么呢?
p1和p2是常量字符串,放在只读数据区,也就是只读,不能被修改,既然不能被改,那么显然每必要在内存中分别开辟两块空间去存放这个字符串,那么p1,p2都将指向字符串的起始位置->a,而arr1 arr2是变量,可以被修改,那么他们两就有自己独立的空间,虽然起始字符都是a,但由于空间位置不同,所以地址也不相同。
2. 指针数组
指针数组是指针还是数组呢?思考这样一个问题,牛奶是牛还是牛奶呢?
指针数组就是用来存放指针的数组,类型可以是多种,例如int* arr[3]
可以存放三个数组,数组的每个元素是int*类型的
有什么用呢?观察如下代码:
int main()
{
int arr1[] = { 1,2,3,4 };
int arr2[] = { 5,6,7,8 };
int arr3[] = { 9,8,7,6 };
int* parr[3] = { arr1,arr2,arr3 };
return 0;
}
其实这时,我们就已经模拟出了一个二维数组
如何拿到这些元素呢?如下:
int main()
{
int arr1[] = { 1,2,3,4 };
int arr2[] = { 5,6,7,8 };
int arr3[] = { 9,8,7,6 };
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 4; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", *(*(parr + i) + j));
}
}
return 0;
}
模拟二维数组:parr+i拿到首元素地址(第几行的地址),解引用(parr+i)就拿到了这行的元素,最后解引用+j,就拿到了这行的每个元素。
这时也不难想到:
*(parr + i) == parr[i];
*(*(parr + i) + j) == parr[i][j];
3. 数组指针
数组指针就是指针,是指向数组的指针
分析一个问题,下面哪个是数组指针?
int main()
{
int* parr1[10];
int(*parr2)[10];
return 0;
}
parr1首先与[10]结合,说明parr1是一个数组,然后与 int星 结合,说明数组的每个元素是 int星 类型;
parr2首先与星号结合,说明parr2是一个指针,然后与 [10] 结合,说明parr2指向的是一个数组,最后与int结合,说明数组的每个元素是 int 类型。
这个数组指针该如何使用呢?
需要先讨论以下数组名:
//数组名通常表示的都是首元素的地址
//但有两个例外:
1.sizeof(数组名) ,(注意:这里严格要求格式,如果是sizeof(数组名+0),这就不算。)这里的数组名表示整个数组,计算的是整个数组的大小。2.&数组名,(这里可以测试一下,通过printf(“%p”,数组名), 这里打印出来和首元素地址是一样的,但表示的意义不一样!)这里的数组表示整个数组,所以这里取出来的是整个数组的地址。
&数组名 怎么理解呢?
因为数组的首元素地址是从数组的起始位置开始的,而数组的地址也是从数组的起始位置开始,否则怎么得到数组的每一个元素呢,但是请注意,他们的意义却天差地别,看如下代码:
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr);
printf("%p\n", &arr+1);
return 0;
}
观察运行结果:
可以观察到
数组名+1 跳过4个字节,也就是跳过数组里的一个元素
原因:数组名表示首元素地址,首元素地址是int * 类型,所以+1跳过一个int*类型的大小,故是跳过4个字节
&数组名 + 1 跳过的是整个数组的大小,跳过了40个字节
原因:&数组名表示整个数组的地址,所以+1跳过整个数组大小,也就是40字节
接下来讨论以下如何创建and使用数组指针:
首先可以想到:
int main()
{
int arr[10] = { 0 };
int* p = arr;
return 0;
}
//这里数组名表示首元素地址,首元素地址是int*类型,故存放在int*这样类型的变量中
那么如果是&数组名呢?
int main()
{
int arr[10] = { 0 };
int* p = arr;
int(*p)[10] = &arr;
return 0;
}
//这是如何写出来的呢?
//首先数组指针就是是用来存放数组的指针
//那么他因该是个指针
//创建一个指针*p
//他因该指向一个数组,所以是(*p)[10],这里由于优先级问题*p要用括号括起来,否则p就会先于[10结合]
//数组每个元素的类型是int,所以最后
//int(*p)[10],这里想必就可以理解了
补充一点:去掉变量名,剩下的就是类型名,比如int(*p)[10],去掉p,剩下的int( * )[10]就是数组指针类型,这个类型也就觉得你+1,-1操作跳过的字节数。
注意不能这样写:
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[] = &arr;
return 0;
}
//会报警告: warning C4048: “int (*)[0]”和“int (*)[10]”数组的下标不同
如何使用:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int(*p)[10] = &arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(*p + i));
}
return 0;
}
//p表示整个数组的地址,*p表示通过整个数组的地址找到了这个数组
//数组由什么来表示呢?当然是数组名啦~
//所以*p本质上就是首元素的地址,所以通过*p+i就可以遍历数组每个元素的地址
不过以上这种用法大家有没有觉得很怪,实际上,一个正常人都不会这么去写的,数组指针实际上不是这么用的,因为以上我们一般最常写,最舒服,最易理解的写法是以下代码:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
那么数组指针到底用在哪里合适呢?
至少是二维数组及以上
看以下代码:
//首先是一般的写法
void print1(int arr[3][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 };
print1(arr, 3, 4);
return 0;
}
//下面是指针的写法
void print2(int(*parr)[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 ", *(*(parr + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
print2(arr, 3, 4);//这里传入的arr是首元素地址,在二维数组中也就是数组第一行的地址
return 0;
}
//注意这里print2函数接收arr时不能用二级指针去接收
//二级指针是用来接收一级指针的地址
//而这里传去的是二维数组第一行的地址
知道了数组指针和指针数组,可以看看下面代码的意思:
int(*parr[10])[5]
//这是什么呢?
//parr先于[10]结合,说明他是一个数组
//那么,去掉数组名和数组元素个数,剩下的int(*)[5]就是数组元素的类型名
//所以parr是一个存放数组指针的数组
最后,可能又有人会问,那这个东西是不是不常用,不重要?
我想说的是,当你将来阅读别人的代码的时候发现有数组指针,这时你就会觉得重要了~
4. 函数指针
函数指针是一个指针,指向的是函数的地址
上面我们提到了&数组名和数组名的区别,那么函数指针是否也具有相同的性质呢?他们之间可以类比吗?
观察如下代码:
int Sub(int a, int b)
{
return a - b;
}
int main()
{
printf("%p\n", Sub);
printf("%p\n", &Sub);
return 0;
}
运行结果:
发现&函数名,和函数名的地址是一样的,那么他们直接有类似数组名的性质吗?
这里要注意,函数名与&函数名的性质和意义毫无差别,都是函数的地址,和数组名与&数组名不能类比。
下面是函数指针的创建and使用:
//创建
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int(*pf)(int, int) = ⋐
return 0;
}
//使用
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int(*pf)(int, int) = ⋐
int ret = (*pf)(1, 2);
printf("%d\n", ret);
return 0;
}
//做到了间接通过函数指针访问函数
//*pf找到了这个函数
//接下来去调用他(*pf)(1,2);
//实际不写*,这样写也行,如下:
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int(*pf)(int, int) = ⋐
int ret = pf(1, 2);
printf("%d\n", ret);
return 0;
}
//至于为什么要写上*呢?
//其实是便于你去更好的理解指针,知道通过对地址的解引用找到相应的变量,但这里,实际上*只是个摆设。
//这里肯定还会有人觉得不理解,为什么能这样呢?看如下代码:
int Sub(int x, int y)
{
return x - y;
}
int main()
{
//int(*pf)(int, int) = ⋐
//int(*pf)(int, int) = Sub;
int ret = Sub(1, 2);
printf("%d\n", ret);
return 0;
}
//在不用指针的情况下,我们进行函数调用的时候通常都写成 函数名(实参)
//我们又知道函数名实际上就是函数的地址
//那么我们用的pf不就是函数的地址吗?
//函数的地址难道不就是函数名吗?
//所以sizeof(pf)与sizeof(&pf)是一样的,地址的大小为4/8;
//有没有一种醍醐灌顶的感觉~ 嘻嘻
这里还有一点值得注意的是pf一定要用(*pf),否则,pf会先于后面的(1 , 2)结合,然后函数调用完后返回一个值,
这个值在被 *号解引用,值怎么能被解引用呢,所以一定要注意。
两个有趣的代码,他们分别是什么意思?
以下代码来自于 ——《C陷阱与缺陷》
//代码一
#include<stdio.h>
int main()
{
( *( void (*)() )0 )();
return 0;
}
//因该从0看起,这里实际上是对0的地址的一个强制类型转换
//转换成void(*)()类型,也就是无参,返回值是viod的型的函数指针类型
//括号外的*,对函数指针进行解引用,右边又有一个括号表示对此函数进行调用
//以上代码本质上是一次函数调用
//代码二
#include<stdio.h>
int main()
{
void (*signal(int, void(*)(int)))(int);
return 0;
}
//先从signal下手,前面有*,后面有(int,void(*)(int))
//首先这里signal不是指针,因为signal会先与后面的()先结合
//即signal是函数名
//这里进行了一次对signal的一次函数声明,为什么不是函数调用呢?因为这里没有实参
//再看外面是void(*)(int)是一个函数指针
//所以,以上代码实际上是一次函数声明
//声明的signal函数的第一个参数是int,第二个参数是函数指针,该函数指针的返回类型的是void,参数是int,signal函数的返回类型是void(*)(int),是一个函数指针,该函数的参数是int,返回类型是void。
5. 函数指针数组
函数指针数组是一个数组,数组的每个元素是函数指针类型
如何去创造一个函数指针数组呢?
//假设有4个函数,分别是Add,Sub,Mul,Div,参数皆为两个int类型,返回值为int
//那么就有如下代码
#include<stdio.h>
int main()
{
int(*parr[4])(int, int) = { Add,Sub,Mul,Div };
return 0;
}
//数组名是parr,parr[4]表示他是一个数组,剩下的就是类型名
//因为数组里面要存放函数指针,故类型为int(*)(int,int);
他有什么用途呢?
接下来,制作一个简易的计算器来说明:
//这里只用函数指针来做
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;
}
void Cal(int(*pf)(int, int),int x,int y)
{
printf("请输入:");
scanf("%d%d", &x, &y);
printf("%d\n", pf(x, y));
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
Cal(Add,x,y);
break;
case 2:
Cal(Sub, x, y);
break;
case 3:
Cal(Mul, x, y);
break;
case 4:
Cal(Div, x, y);
break;
default:
break;
}
} while (input);
return 0;
}
如果用函数指针数组就可以减少代码量,使其更精炼.
//用函数指针数组
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;
int(*pfarr[])(int, int) = { 0,Add,Sub,Mul,Div };
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if (input == 0)
{
printf("退出程序\n");
break;
}
else if (input >= 1 && input <= 4)
{
printf("请输入数字:");
scanf("%d%d", &x, &y);
printf("%d\n",pfarr[input](x, y));
}//这里通过pfarr[input]找到相应的数组元素,
//函数指针用法等于函数名用法,所以后面直接用括号调用
} while (input);
return 0;
}
这样看是不是要精炼很多呢~
6. 指向函数指针数组的指针
指向函数指针数组的指针就是一个指针,指向的是函数指针数组,数组的每个元素是函数指针
套娃开始~
长啥样嘞?
//这是函数指针数组
int(*parr[4])(int, int) = { Add,Sub,Mul,Div };
//这是指向函数指针数组的指针
int(*(*pparr)[4])(int, int) = &parr;
//首先pparr与*结合,说明是个指针
//再与[4]结合,说明是个数组
//数组的每个元素是什么类型呢?挖掉中间刚刚分析的部分
//剩下int(*)(int, int),说名数组的每个元素是一个函数指针类型
//指针指向的是两个个参数为int,返回值为int的函数
指向函数指针数组的指针就讲到这里,因为在讲下去,太多啦,就是无限套娃。
为什么这么说呢?
想象一下,指向函数指针数组的指针,实际上是一个指针,那么,是不是可以把这些指针通过数组存起来呢?叫做指向函数指针数组的指针的数组,那么,是不是还可以把这个数组的地址通过只一个指针存起来呢…
7. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
可能细心的小伙伴已经发现啦,在制作简易计算器的时候,通过函数指针的方式用到过回调函数
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;
}
void Cal(int(*pf)(int, int),int x,int y)
{
printf("请输入:");
scanf("%d%d", &x, &y);
printf("%d\n", pf(x, y));
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
Cal(Add,x,y);
break;
case 2:
Cal(Sub, x, y);
break;
case 3:
Cal(Mul, x, y);
break;
case 4:
Cal(Div, x, y);
break;
default:
break;
}
} while (input);
return 0;
}
//这里通过Cal接收函数指针,并在本函数内部调用这个函数指针所指向的函数,这里过程就是回调函数的过程