指针的深入理解(四)
文章目录
前言
哈喽,各位小伙伴大家好!今天小编带着大家继续学习指针的内容。经过前几期的学习,我们对指针的知识点掌握得差不多了。今天我们把剩下的内容继续学习。向着大厂进发!
一.数组指针
1.1数组指针的概念
什么是数组指针?我们之前学过很多类型的指针,我们不妨类比一下。
通过类比我们得知,数组指针是一种指针变量,是用来存放数组地址的指针变量。
1.2数组指针的初始化
我们知道指针存放的是地址。所以数组指针存放的是数组的地址。所以我们就需要把数组的地址赋值给数组指针,这就是数组指针的初始化。那数组的地址怎么表示呢?我们之前说过&数组名表示取出的是整个数组的地址。所以数组的初始化应该是这样写。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4 };
p = &arr;//&arr表示整个数组的地址
return 0;
}
直接&arr赋值给指针p。但是我们还没给p明确类型。p是数组指针,那他的类型就是数组指针类型。
1.3数组指针类型
数组指针类型该怎么写呢?首先p是指针那我们就让p和*结合,说明p是个指针。再在星号p后面写上方括号[],说明指针指向一个数组。那数组有几个元素呢?方括号里面的数字就代表指向数组的元素数。那数组元素的类型是什么呢?最前面的类型加粗样式就是数组元素的类型。所以数组指针类型我们可以这样写。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[4] = { 1,2,3,4 };
int (*p)[4] = &arr;//&arr表示整个数组的地址
//*表示是个指针[4],表示指向的数组4个元素,4不可省略
//int表示数组元素类型是int类型
return 0;
}
注意方括号里的数组表示指向数组的元素个数,所以不可省略,并且指向的数组元素不同,即使类型相同指针变量的类型也是不同的。变量名p去掉后剩下的就是数组指针类型。
- 字符数组指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char arr[] = "abcd";
char (*p)[5] = &arr;//&arr表示整个数组的地址
//指针类型:char (*)[5]//注意是五个字符的数组才是这样
return 0;
}
指针类型为:char (*)[具体数组元素个数]。
- 整型数组指针
#include<stdio.h>
int main()
{
int arr[4] = { 1,2,3,4 };
int (*p)[4] = &arr;//&arr表示整个数组的地址
//指针类型:char (*)[4]//注意是4个整型的数组才是这样
return 0;
}
- 指针类型为:int (*)[具体数组元素个数]。
1.4数组指针类型的理解
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4 };
printf("%d\n", arr);//数组名表示数组首元素的地址
printf("%d\n", arr+1);//指向的是数组元素+1,跳过一个元素,4字节
printf("%d\n", &arr[0]);//取出数组首元素的地址
printf("%d\n", &arr[0] + 1);//指向的是数组元素+1,跳过一个元素,4字节
printf("%d\n", &arr);//&数组名表示整个数组的地址,但也是指向数组首元素地址
printf("%d\n", &arr + 1);//指向的是数组+1,跳过一个数组,16字节
return 0;
}
以这个代码为例。大家看一下代码结果是啥?
为什么会出现这样的结果呢?
我们知道指针类型决定了指针加1向前移动多大距离。&arr的指针类型是int (p)[4*]指向一个四个整形的数组,所以+1跳过整个数组,也就是4个整型,16个字节。
- 结论一:指针类型决定指针加1向前移动多大距离,变量名去掉后就是数组指针的类型。
二.二维数组的本质
二维数组本质其实就是一个特殊的一维数组。数组每个的元素是一个一维数组。以下面的代码为例。
void test(int arr[3][5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
这是我们常用的二维数组使用方式。现在我们可以根据二维数组的本质进行改写。
传的是数组名,数组名代表首元素地址。二维数组首元素是个一维数组,那我们就用数组指针接收。我们在用星号(arr+i)访问第i行数组,星号(*(arr+i)+j)访问第i行第j个元素。所以代码就可以写成这样。
void test(int (*p)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
成功输出二维数组。
-
结论二:二维数组是一个特殊的一维数组,数组每个元素是一个一维数组。
-
结论三:二维数组传参的本质是第一行的地址,写成数组的形式是为了方便理解。
三.函数指针
3.1函数指针的理解
函数指针是啥?这里我们还是进行类比。
类比之后得出结论:
- 结论四:函数指针是一种指向函数的指针变量,存放的是函数的地址。
3.2函数的地址
函数也是有地址的,函数的地址可以通过&函数名得到。那我们再来类比。数组名和&数组名都指向数组首元素的地址,在数值上相等,那函数名是不是也是个地址,也和&函数名在数值相等。我们来验证一下猜想。
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
以这个加法函数为例。
大家发现结果是一样的,证明函数名和&函数名都表示函数的地址。
- 结论五:函数名和&函数名都表示函数的地址。
3.3函数指针变量
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = &Add;
return 0;
}
函数指针是个指针,所以先和*结合,后面是函数参数,前面是函数返回类型。函数名去掉后剩下的就是函数指针类型。
- 结论六:函数指针类型:(函数返回类型)(*指针名)(函数参数类型)
3.4函数指针调用
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = &Add;
int (*p1)(int, int) = Add;
int ret = (*p)(5, 5);
int ret1 = (*p1)(5, 5);
int ret2 = Add(5, 5);
printf("%d\n", ret);
printf("%d\n", ret1);
printf("%d\n", ret2);
return 0;
}
函数指针调用时,先对指针解引用找到函数,再在后面传参给函数即可完成函数调用。
大家观察一下正常函数调用时Add(5,5),Add是函数名,函数名就是函数地址。那是不是函数地址后面传参就能完成函数调用了?如果是的话,Add赋值给p1,p1等价于函数地址,那是不是p1(5,5)就能完成函数调用呢?
结果是一样的,说明我们的猜想是对的。函数指针调用时星号可以省略。
四.两端有趣的代码
4.1代码一
int main()
{
(* ( void (*) () ) 0 )();//函数调用
//1.将0强制类型转换为void (*) () 类型的函数指针
//2.调用0处地址的函数
return 0;
}
大家来看这个代码是啥意思?是不是觉得看不懂?没事我来带大家解读这个代码是啥意思。
先将0强制类型转化为void(*)()类型的函数指针。
再解引用调用0地址处的函数。
所以这代码是一次函数调用。
4.2代码二
int main()
{
void ( *signal(int, void(*)(int) ) )(int);//函数声明
//声明的函数名字是signal
//signal函数有两个参数,一个是int,
//一个是void(*)(int)函数指针类型,指针指向的函数参数为int,返回类型为void
//signal函数的返回类型为void(*)(int)
return 0;
}
大家再来看第二段代码。大家认为这段代码又是在干嘛?
大家把signal看成函数名,那int就是一个函数参数,
void(*)(int)也是一个参数,圆圈外的就是函数的返回类型。
返回类型为void(*)(int)。
那是不是写成这样更方便理解呢?
int main()
{
void(*)(int) signal(int, void(*)(int));//函数声明
//声明的函数名字是signal
//signal函数有两个参数,一个是int,
//一个是void(*)(int)函数指针类型,指针指向的函数参数为int,返回类型为void
//signal函数的返回类型为void(*)(int)
return 0;
}
但是大家发现编译器报错了,说明编译器不支持这种语法。当函数返回类型为函数指针时,函数名和参数写在星号旁边,也就时第一种写法。那有什么办法可以更方便理解吗?这就用到我们的typedef关键字了。
五.typedef关键字
typedef是用来对类型重命名的,可以将复杂的类型简单化。
5.1无符号整除重命名
int main()
{
typedef unsigned int un;
unsigned int num1;
un num2;
return 0;
}
如果我们觉得每次都写unsigned in太麻烦时,就可以将unsigned int
重新typedef对unsigned in类型重命名。
5.2数组指针类型重命名
int main()
{
typedef int(*parr_t)[5];//名字放星号旁边
int arr[5] = { 0 };
int(*p)[5] = arr;
parr_t p1 = arr;
return 0;
}
这里我们对数组指针类型重命名,但是要注意的时数组指针类型和函数数组指针类型重命名时名字要放在星号旁边。
5.3函数指针类型重命名
int main()
{
typedef void(*pf_t)(int);
pf_t signal (int,pf_t);
void(*signal(int, void(*)(int)))(int);
return 0;
}
这里我们用typedef关键字对代码二进行类型重命名,注意重命名的名字要放在星号旁边,此时两个代码等价。
5.4#define和typedef的区别
我们知道#define的功能和typedef十分相似,那他们的区别在哪里呢?我们这里以一段代码为例。
typedef int* prt_t;
#define PTR_T int*
int main()
{
prt_t p1, p2;//p1,p2都是整型指针
PTR_T p3, p4; //p1是整型指针,p2是整型
//int * p3,p4
//星号只能给p3用,p4用不了
return 0;
}
可以看到p4的类型为int,而不是int*,这是因为#define定义p3和p4时,星号只能给p3,给不了p4 。相信大家看到这里也能感受到define和typedef的区别了。
后言
今天给大家分享的内容有点多,感谢各位小伙伴的耐心阅读。到这里指针的内柔我们已经快讲完了。坚持就是胜利!好啦,今天就分享到这了。咱们下期见!拜拜~