指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:
这个章节,我们继续探讨指针的高级主题。
提示:以下是本篇文章正文内容,下面案例可供参考
常规使用
#include
int main()
{
char ch = 'w';
char* pc = &ch;
//用char*类型的pc来接收char类型ch的地址
//pc指向一个字符变量ch
return 0;
}
#include
int main()
{
char* p = "hello bit";//ps:bit后面还默认有一个\0
//char*类型一共4字节,所以这里并不是把整个hello bit\0放到p指针变量里去
//真正的意思是:把常量字符串hello bit首字母h的地址赋给了p
printf("%c\n", *p);//打印h
printf("%s\n", p);//打印hello bit
//%s是从提供的地址(这里是首字母h的地址)开始,往后找\0,找到\0打印结束
return 0;
}
示意图如下:
注:"hello bit"这是一个常量字符串,而对于常量,我们是要求不能修改的,我们上述代码中是把h的地址给了P,那么通过*P是可以修改常量字符串内容的,一旦进行修改,系统就会报错。
所以,更加妥善的写法是const char * p = “hello bit”;
趁热打铁,我们来看一道字符指针的面试题:
#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;
}
解释1:
数组是独立开辟空间的,str1和str2分别是两个字符数组的数组名,数组名是数组首元素地址,str1和str2分别是两个不同数组首元素的地址,自然是不一样的。
解释2:
str3和str4都是const修饰的char*指针,这两个指针都指向一个常量字符串。
而常量字符串又是不能修改的,也就是它的内容永远是"hello bit",那么我其他的指针也想来指向这个不可修改的"hello bit"时,为了节省内存,就没必要再创建一块额外空间了,还指向原来的那个即可。
指针数组是指针还是数组?
这个问题你就想,好男孩,好男孩的本质是男孩啊
指针数组本质还是数组
举个例子:
int main()
{
int arr1[5];//整形数组,存放整形的数组就是整形数组
char arr2[3];//字符数组,存放字符的数组就是字符数组
//指针数组,存放指针的数组就是指针数组
int* parr[5];//整形指针数组
char* pbrr[4];//字符指针数组
return 0;
}
int main()
{
int a = 123;
int b = 213;
int c = 312;
int* arr[3] = { &a,&b,&c };
for (int i = 0;i < 3;i++)
{
printf("%d\n", *arr[i]);
//arr[i]是一个元素地址,*arr[i]对该地址解引用,得到地址指向的元素
}
return 0;
}
应用实例2:
#include
int main()
{
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
int* parr[] = { arr1,arr2,arr3 };//数组名是数组首元素地址,这里类型为int*
for (int i = 0;i < 3;i++)
{
printf("%d\n", *parr[i]);
//parr[i]是一个元素地址,*parr[i]对该地址解引用,得到地址指向的元素
}
return 0;
}
如果想得到三个数组的所有元素
#include
int main()
{
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
int* parr[] = { arr1,arr2,arr3 };//数组名是数组首元素地址,这里类型为int*
for (int i = 0;i < 3;i++)
{
for(int j=0;j<3;j++)
{
printf("%d ", parr[i][j]);
//举个例子:
//parr[0]是arr1,
//parr[0][1]就是arr1[1],即2
}
printf("\n");
}
return 0;
}
ps:还有一个老生常谈的话题:
arr[i]=*(arr+i),因为arr是数组名(一个地址)嘛,
arr+i就是这个地址往后i个单位,也就是arr[i]的地址,
对(arr+i)解引用,就可以得到arr[i]
应用实例3:
#include
int main()
{
const char* arr[5] = { "abc","defg","hijkl","mn","opqish" };
//ps:每个常量字符串后面都默认有一个\0,比如“abc”其实存储的是abc\0
int i = 0;
for (i = 0;i < 5;i++)
{
printf("%s\n", arr[i]);//arr[i]是每个字符串首元素地址
}
return 0;
}
数组指针是数组还是指针?
还是那个判别方法:好男孩,性质是好,本质是男孩
所以,数组指针本质上是个指针
int *p1[10];//指针数组
int (*p2)[10];//数组指针
解释:p2先和 * 结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。
所以p2是一个指针,指向一个数组,叫数组指针。
这里要注意:[]的优先级要高于 * 号的,所以必须加上()来保证p先和*结合。
示例:
#include
int main()
{
int a = 10;
int* pa = &a;//整形地址放整形指针中
char c = 'w';
char* pc = &c;//字符地址放字符指针中
int arr[10] = { 0 };
int* p = arr;//p是数组arr首元素的地址
int(*parr)[10] = &arr;//数组地址放在数组指针中
//简单判断类型的方法就是把变量名去掉,剩下的就是类型名
//比如int a,把a去掉,剩下int就是a的类型
//parr类型是int(*)[],
return 0;
}
#include
int main()
{
int arr[10] = { 0 };
//arr 是首元素地址
//&arr[0] 是首元素地址
//&arr 是整个数组地址
printf("%p\n", arr);//arr类型是int*
printf("%p\n", &arr[0]);//&arr[0]类型是int*
printf("%p\n", &arr);//&arr类型是int(*)[10]
printf("=====我是分割线====\n");
printf("%p\n", arr+1);
printf("%p\n", &arr[0]+1);
printf("%p\n", &arr+1);
return 0;
}
综上,我们可以了解到,数组首元素地址和整个数组地址的区别:
他们在进行±操作时,跳过的单位是不同的,
前者是跳过一个数组元素大小,后者则是跳过整个数组的大小。
数组名不是数组首元素地址的两种特殊情况:
#include
int main()
{
//情况1
//sizeof(数组名),这里数组名表示整个数组
int arr[10] = { 0 };
printf("%d\n", sizeof(arr));//打印40
//情况2
//&数组名,这里数组名表示整个数组,&arr取出的是整个数组的地址
return 0;
}
引子:
#include
int main()
{
int* arr[10] = { 0 };
//这里arr是一个指针数组
//怎么判断?联想int brr[10],这里brr是一个数组,里面存放10个int类型的元素
//那么相应的, int* arr[10],这里arr是一个数组,里面存放10个int*类型的元素
int* (*p)[10] = &arr;
//arr是一个指针数组,&arr获得了指针数组的地址
//如果想把这个地址赋给变量p
//那么p的类型一定是一个指针,所以我们先用(*p),让p变成一个指针
//而p这个指针指向的是一个大小为10的指针数组啊,所以外面套一层int* [10]
//最终p的类型就是int* (*p)[10]
int** p1 = arr;
//arr是指针数组,
//这里直接用arr表示数组首元素地址,也就是指针的地址,所以我们用int**
return 0;
}
示例1:
#include
void print(int(*parr)[10],int sz)
{
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", parr[0][i]);
//把arr[10]看成一个二维数组,也就是1行10列的二维数组
//那么*(parr+0)也就是arr
//*(parr+0)==parr[0]
//arr[i]==parr[0][i]
//所以上面parr[0][i]写成(*(parr+0))[i]或者(*parr)[i]也是可以的
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(&arr,sz);
return 0;
}
示例2:
void print(int(*p)[5],int r,int c)//r为行数,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("%d ", *(*(p + i) + j));
//也可写printf("%d ", p[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
//二维数组行可省略,列不可省略
print(arr, 3, 5);
//二维数组的数组名作为参数,传的是第一行一维数组的地址,也就是arr[0][5]的地址
return 0;
}
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
//注:[]优先级比*高
int arr[5];//整型数组
int *parr1[10];//指针数组
int (*parr2)[10];//数组指针
int (*parr3[10])[5];
对于parr3,它首先是和[10]结合,我们就可以确定它本质是一个10元素的数组
然后把int ( * parr3[10] )[5]中的parr3[10]去掉就可以得到int ( * )[5],
也就是数组元素为数组指针,这种指针指向数组有5个元素,每个元素为int型
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
void test(int arr[])//数组传参,数组接收,没有任何问题
{}
void test(int arr[10])//10个大小的接收10个大小的数组,也没有问题
{}
//其实这里arr[]里面放100,1000都没问题,只不过看起来有点别扭,
//主流写法还是int arr[]来接收
void test(int *arr)
{}
//我们之前说过,数组名作为参数表示数组首元素地址,所以这里int*接收没问题
void test2(int *arr[20])
{}
//这个参数和int* arr[]是一个效果,因为是整形指针数组作为参数,
//那么我们用同样的整形指针数组来接收也没有问题
void test2(int **arr)
{}
//整形指针数组首元素是整形指针,而数组名又表示首元素地址,
//也就是整形指针的指针,这样写也没问题
int main()
{
int arr[10] = { 0 };
int *arr2[20] = { 0 };
test(arr);
test2(arr2);
return 0;
}
热知识:二维数组首元素指的是该二维数组的第一行
void test(int arr[3][5])
{}
//3*5的数组传参过去,3*5的数组来接收,没有任何问题
void test(int arr[][5])
{}
//这种写法和一维数组[]里面放不放数字一样,
//但是二维数组,函数形参的设计只能省略行,不能省略列
//举个例子,我现在有15个元素,我知道是3列,可以算出来,行就是15/3=5个
//但是如果知道是3行,妈的,你一行可以放15个,也可以一行只放1个,谁知道你要放多少列?
void test(int(*arr)[5])
{}
//数组名作为参数,传的是首元素地址。
//对于二维数组来说,数组首元素是第一行的地址,
//我们这里是3*5的整形数组,那么它的第一行就是1*5的整形数组
//我们用整形数组指针来接收
int main()
{
int arr[3][5] = { 0 };
test(arr);
}
#include
void print(int *p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int *p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
#include
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;//pp为二级指针
test(pp);
test(&p);//如果接收方是二级指针,我们传一级指针地址也可以
return 0;
}
示例:我们常常接触的指针大多有如下几类:
整形指针-存放整形地址,指向整形
字符指针-存放字符地址,指向字符
数组指针-存放数组地址(注意不是数组首元素地址),指向数组
由以上三个例子,我们能总结指针的共同点:存放某个类型变量的地址,指向那个类型的变量,但是在讲函数指针首先有一个问题:函数也有地址吗?我们用一段简单的代码来验证一下即可。
#include
int Add(int x,int y)
{
return x+y;
}
int main()
{
printf("%p\n",&Add);
return 0;
}
所以答案是有的,函数也存在地址,也就衍生出了今天的知识点-函数指针。
我们知道&数组名,取出的是数组的地址。单独一个数组名,取出的是数组首元素的地址。但是对于函数来说:函数名==&函数名
我们代码验证一下(示例):
#include
int Add(int x,int y)
{
return x+y;
}
int main()
{
printf("%p\n",&Add);
printf("%p\n",Add);
return 0;
}
显然,打印出来的地址是一样的,但是这个时候也会有同学跳出来说:“那数组名和&数组名打印出来的地址还一样呢,但意义明显不一样啊”。但是你想想,函数也没有首元素等其他玩意啊,它就是它本身啊,它也不会出现什么函数首元素啊。
所以再次声明:
在函数指针这一块 函数名==&函数名,它的意义和值,都是一样的
我们知道,数组指针用来存放数组地址,整形指针用来存放整形地址。。。函数指针也不例外,它用来存放函数地址,我们现在定义一个p来存放Add地址,那它的类型怎么创建?我们来看一下具体步骤:
1.p是一个指针对吧,给它一个*是不是必须的 p变成了 * p。为了确保 * 和 p结合(如果没有括号,*或者p有可能会与其他的一些符号结合,具体参见符号优先级)那我在 * p外面加一个括号便于观看也没有问题吧,也就是(*p)
2.那函数总得有参数啊,比如这里是Add(int x,int y)。参数x和y的类型是int
你指针指向的函数是不是要找一下它的参数。所以(*p)(int,int)
3.那函数还有一个性质啊,有没有返回值,要是有的话,类型呢? 这里以Add为例,它是返回int型,所以我们指针也返回int 型 即int(*p)(int,int)
到这里Add函数指针的类型就创建完成啦即为*int(p)(int ,int)
需要注意的是:不同函数的参数类型和返回值类型是不一样的,到时候需要根据不同函数对类型进行转换,这里只是以Add函数为例,其他函数以此类推
ps:一个快速判别类型的方法——去掉变量的名字,剩下的就是类型
代码如下(示例):
int a = 10;//去掉a 类型int
int arr[10] = { 0 };//去掉arr 类型int [10]
int(*parr)[10] = &arr;//去掉parr 类型int(*)[10],数组指针,指向一个10int型元素的数组
int(*pf)(int, int) = &Add;//去掉pf 类型int(*)(int,int)
法一:
我们平时在调用函数的时候,一般就是函数名( ,)然后把参数传进括号即可,那我们现在有函数指针了呀,指针怎么使用?
p不是指向了函数Add嘛,我们用*解引用指针,得到的是地址里的东西,
也就是说 *p==Add,用 * p(,)来传参也可以实现Add函数的调用。
代码如下:
#include
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(2, 3);
printf("%d\n", ret);//ret=5
int(*p)(int, int) = &Add;//p是一个指向函数Add的指针
ret = (*p)(3, 3);//ret=6
//p指向Add,对p解引用就是Add
//简言之:*p=Add
//我们并不总是可以拿到变量,有时是拿到变量的地址
//对应函数指针同样的道理,有时不直接给你函数,给你函数地址,就这样调用
printf("%d\n", ret);
}
法二:
我们在二.1取函数地址那一块介绍了,在函数指针这一块,函数名==&函数名, 也就是说创建函数指针的时候可以这样写:int(*p)(int, int) = Add,Add是赋给了p啊,你也可以认为:p就是Add。
你可以这样理解,法一是int(*p)(int, int) = &Add,是把Add的地址给p,所以用p来调用函数要解引用一下,但是法二p就是Add,那不用解引用了,直接调用。代码如下:
#include
int Add(int x, int y)
{
return x + y;
}
int main()
{
//我们由前面的知识知道:函数add取地址时,add=&add
int(*p)(int, int) = Add;//把Add赋给p,这里p即可看做Add
//与法一不同的是,法一将&Add赋给p,p是Add的地址,
//所以要解引用,这里p就可以看做是Add本身,可以不解引用
int ret = p(3, 6);
printf("%d", ret);
}//如果是为了方便理解,一般是用第一种方法,
//如果是为了操作方便,可以用第二种方法
大家来看这样一个代码
( * (void(*)() ) 0)()
乍一看非常复杂,我们来细化一下
1 . ( * (void( * )() ) 0)() 我们抽出加粗部分
这是我们熟悉的老朋友:void( * )(),这不就是一个函数指针嘛,该函数无参,返回类型void
2 . (void( * )() ) 0是什么?我们联想一下(int)3.14,不就是对3.14强制类型转换嘛,将3.14这个浮点型强制转换成整形。这里同样的道理,是将整形0强制转换成类型为void( * )()的一个函数指针
3 .现在有了(void( * )() ) 0,我们在这个东西前面加一个 *,这个是什么意思,我们知道(void( * )() ) 0已经被转换成一个指针(指针即地址)了,地址前面加一个 *表示解引用,取出地址里的东西,也就是找到了那个函数
4 . (void( * )() ) 0表示那个函数那再在后面加一个()即是对函数的调用,也就是( * (void(*)() ) 0)()
示例:以下这个代码,我们可以很清楚的看出,arr是一个整形指针数组,数组里的每个元素均为整形指针
int *arr[10];
那我们就会有接下来的两个问题:
整形指针能放入到一个数组内,
那么参数、返回类型相同的*函数指针能否也可以放入一个数组内?
如果可以,我们应该如何创建那个数组呢?
先上代码(示例):
#include
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int(*p1)(int, int) = Add;//Add和Sub的函数指针类型为int(*)(int,int)
int(*p2)(int, int) = Sub;
int(*parr[2])(int,int)={Add,Sub};
}
由以上代码,我们不难看出,函数Add和函数Sub除了名字和功能不同,它们的参数和返回类型均相同,我们可以写出它们共同的函数指针类型(详情见上一期的C语言快速入门):int(*)(int,int) ,也就是说,只要我们由Add和Sub两个函数的地址,我们都可以放到int( * )(int,int)这个类型中去
那么Add和Sub这两个函数指针数组类型怎么创建?我们举两个简单的例子:
int a;//将变量名a删去就是类型 int也就是整形
int arr[10];//将变量名arr删去就是类型 int [10]也就是整形数组
整形和整形数组类型的差别也就是多了一个[ ]这样的东西
这里找函数指针数组类型的创建法也是同样的道理,
我们仍以Add函数为例,已知了Add函数指针类型为int( * )(int,int),我们在 * 后跟一个[ ]即可
也就是如下代码,初始化和别的数组初始化没有什么区别,
我们正常把函数Add和Sub放入(函数名==&函数名,详情见上一期函数指针)
int(*parr[2])(int,int)={Add,Sub};//这里初始化放入的是函数的地址
//parr就是一个函数指针数组
比如我们这里写一个加减乘除的简易计算器
代码如下(示例):
Add Sub Mul Div分别代表加减乘除
#include
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 ***\n");
printf("***2.Sub ***\n");
printf("***3.Mul ***\n");
printf("***4.Div ***\n");
printf("***0.exit ***\n");
}
int main()
{
int input = 0;
do
{
int x = 0;
int y = 0;
int z = 0;
menu();
printf("请选择:");
scanf("%d", &input);
int(*parr[5])(int, int) = { 0,Add,Sub,Mul,Div };
//这里为什么要加0,是为了让数组中Add到Div下标分别是1到4,对应计算器菜单中的选项
if (input == 0)
{
printf("退出计算器\n");
}
else if (input >= 1 && input <= 4)
{
printf("请输入2个操作数\n");
scanf("%d %d", &x, &y);
z=parr[input](x,y);
//parr是存放函数指针的数组,有了下标也就有了某个函数指针(也就是函数地址)
//我们在函数指针那一块讲解过:调用函数的时候可以直接用函数地址调用
printf("计算结果为%d\n", z);
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
该段代码有以下一些注意点:
1.在写函数指针数组时,因为下标的缘故,菜单中的选项和实际存放的函数指针是不匹配的,我们可以在Add前加一个0(该0本身是无意义的)将下标依次加1个
2.parr是存放函数指针的数组,有了下标也就有了某个函数指针(也就是函数地址)我们在函数指针那一块讲解过:&Add=Add,调用函数的时候可以直接用函数地址调用(详情见上一期C语言快速入门)
这里用函数指针数组写的好处是什么?
在日常的工作中,我们如果要写一个需要用到很多函数的程序,而这个程序将来也极其有可能增加新的或删减旧的函数。传统的更改方式,一旦不小心,可能会损伤到其他正常的代码,导致满盘皆输。如果用函数指针数组,我们只需要对函数指针数组大小变换一下,再将新的或旧的在函数指针数组初始化中修改一下即可,而在函数调用方面也不需进行更改,非常非常非常的方便。
这里开始就出现无限套娃了,其实方法都是一样的。
这里我们只展示如何定义,函数指针数组的指针也就是函数指针数组取地址
int add(int x, int y)
{
return x + y;
}
int (*pf)(int, int) = add;//pf是函数指针
int (*pfArr[5])(int, int);//pfArr是一个函数指针的数组
int(*(*ppfArr)[5])(int, int) = &pfArr;//ppfArr是一个指向函数指针数组的指针
回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,
当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
题库整理1
题库整理2
题库整理3
本文着重介绍了指针数组、数组指针、函数指针、函数指针数组等知识点。指针作为C语言学习的大头,需要读者们认真细致的学习,而一些指针/或者数组的套娃定义也是其中的难点,建议读者们在学习完本文后,认真练习本文中第九节的笔试题,借此提升自己对指针的理解。