🎇作者:小树苗渴望变成参天大树
🎊作者宣言:认真写好每一篇博客
🎉 作者gitee:link
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
指针进阶
- 💦前言
- 💖一、字符指针
- ✨二、指针数组
- 💨三、数组指针
- 💢3.1数组指针的定义
- 💥3.2数组指针的使用
- 💤四、数组参数,指针参数
- 🎄4.1一维数组的传参
- 🧨4.2二维数组传参
- 💞4.3一级指针传参
- 🎞️4.4二级指针的传参
- 🎟️五、函数指针
- ❤️🩹5.1函数指针的定义
- 🎀5.2函数指针的使用
- 🎗️5.3通过有趣的代码来更好的理解函数指针
- 🎫六、函数指针数组
- 🏀6.1函数指针数组的定义
- 🌭6.2用法即用途
- 🍟七、指向函数指针数组的指针
- 🍔 八、总结
💦前言
各位友友们我们又见面了,今天博主继续给大家分享新的知识,并且干货很多,大家一定要好好的消化,我们今天讲的主题还是指针,不过是更高一级的指针,如果友友们连基本的指针都还没有搞得明白,可以去看看我浅谈指针那篇博客,不然看这篇博客会非常吃力。 话不多说,我们开始进入正文。
本篇重点
- 字符指针
- 数组指针
- 指针数组
- 数组传参和指针传参
- 函数指针
- 函数指针数组
- 指向函数指针数组的指针
- 回调函数
- 指针和数组面试题的解析
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)。
- 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
- 指针的运算。
在那篇博客中,我们大概只了解了指针的这些概念,接下来我们继续探讨更高级的指针
💖一、字符指针
字符指针就是指向字符的指针,在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一.一种用法
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
就是把字符的地址给一个指针变量,来存放它的地址,这个就是字符指针
(回忆:*就是告诉p是一个指针,char表示p指向的类型是字符类型)
二.这是字符指针的一种用法,他还有一种特别的用法:
int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
//这里在打印的时候不能写成*pstr,因为你这样只能获得首元素,但你又以字符串的形式打印,所以可能会出错。
return 0;
}
字符串在内存的存储时连续的,把一个字符串赋给一个字符指针p里面,相当于把字符串里面的地址交给了p,那字符串地址又是什么呢?他其实就是首字符的地址,我们只需要访问首元素的地址,就能访问到整个字符串可以想象成数组,当不能当成数组。
这是一个常量字符串,时直接放在内存空间的,里面的内容不能同别人来访问,所以我们需要加一个const进行修饰,防止我们在写代码的时候被别人修改,写了的话,别人在修改的时候编译器就会报错,不然编译器就会崩掉的。
我们来看一道这样的面试题
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const 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;
}
我们来分析一下:
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。相信,大家应该都明白了
那我们接下来在回顾一下之前的内容
✨二、指针数组
在《浅谈指针》博客中我们也学了指针数组,指针数组是一个存放指针的数组
char*p1="abcd";
char*p2="qwer";
我们把两个指针放在一个数组里面
char*arr[2]={p1,p2};
//优化后const char*arr[2]={p1,p2};
int i=0;
for(i=0;i<2;i++)
{
printf("%d",arr[i]);
}
这样是不就可以打印每个元素了
我们把元素地址放在一个数组里面就构成了指针数组了,讲这个主要是和下面的知识点进行对应
💨三、数组指针
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
💢3.1数组指针的定义
我们在进行整型指针的时候,对于intp,先写一个变量,在变量前面加一个来告诉我们这个变量是一个指针,然后在加上所指向地址的类型是int,对于指针变量p来说他的类型是int*
那我们在写数组指针的时侯是不是要知道数组的类型是什么,在数据的存储的时候我提到过构造类型里面又数组,比如int arr[10],这个数组的类型是int [10];一次类推,去掉数组名就是数组的地址。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
int (*p)[10];
解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组(int [10]是数组类型)。所以p是一个
指针,指向一个数组,叫数组指针。
所以p1是指针数组,类型为int *[10]
p2是数组指针,类型为int(*)[10]
这里要注意:[]的优先级要高于号的,所以必须加上()来保证p先和结合。
那我们就来思考了,既然是指针那么就要存放地址了,那我们要存的是数组的地址,那我们来看看下面那个是正确的:
int arr[10]={0};
int (*p2)[10]=arr;//1
int (*p2)[10]=&arr//2
我们来分析第一个,arr表示首元素地址,首元素地址不能代表整个元素的地址啊,所以这个写法是错误的。
第二个,我们所过数组名在两种特殊情况是代表整个数组的地址,一个是&arr,一个是sizeof(arr),所以第二中写法是符合要求的。
💥3.2数组指针的使用
看代码:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p=arr;
int i=0;
for(i=0;i<10;i++)
{
printf("%d",*(p+i));//打印每一个元素
}
return 0;
}
根据数组的地址是连续的性质,我们通过指针来访问我们的数组
那我们再来看一种访问数组的方式:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;
int i=0;
for(i=0;i<10;i++)
{
printf("%d",(*p)[i]);//打印每一个元素
}
return 0;
}
我们在进行*p的时候得到整个数组里面的内容,有的人会说都得到里面的内容直接打印出来不就好了,为什么还要通过下标来访问,原因是这是一个整型数组不想字符串数组,这个没有结束标志,停不下来,所以我们需要通过下标循环的方式来访问数组里面的每一个元素。
那既然我们访问数组还是需要通过下边来访问数组里面的每一个元素,那我们为什么不使用简单一点的第一种呢?所以数组指针对于一维数组的使用就显得有点不太友好,所以我们经常在二维数组上去使用。
我们来看一段代码:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
我们通过这个图片很好的知道了二维的数组指针怎么传参的,并且也知道怎么访问数组里面的每一个元素了吧
注意:
我们刚才提到二维数组的数组名是首元素的地址,也就是第一行的地址,那既然是地址,我们为什么不能使用一级指针存储呢??原因是一级指针只能存储一个元素的地址,不能存放多个元素的地址,只能使用指针数组来存储多个元素的数组。数组指针不受数组里面元素的限制
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
1.arr是一个数组里面有5个元素,每个原不俗的类型为int
2.parr1是一个指针数组,里面有10个元素,每个元素的类型是int *
3.parr2是一个数组指针,指向的数组里面有10个元素,每个元素的类型是int。 parr2的类型为int()[10]
4.parr3先和[]结合是一个数组里面有10个元素,每个元素是一个数组指针(相当于一个数组的整个地址,这个数组有5个元素)parr3这个数组的类型是int()[5]
不懂的可以看看这个图片理解一下
因为是数组指针,涉及到数组,数组就两种形式,对一维数组不太友好,所以我们掌握二维数组的数组指针怎么使用就好了。我们接下来在借一个新的知识点
💤四、数组参数,指针参数
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
🎄4.1一维数组的传参
#include <stdio.h>
void test(int arr[])//ok?
{}//用数组接收肯定是没问题的
void test(int arr[10])//ok?
{}//这个更没有问题了
void test(int *arr)//ok?
{}//传过来是arr首元素地址,用一级指针接收没有问题
void test2(int *arr[20])//ok?
{}//本身就是一个指针数组,用指针数组接收,没有问题。
void test2(int **arr)//ok?
{}//传过来是首元素地址,首元素的类型是int*类型,相当于传过来一级指针的地址,用二级指针接收,没有问题
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
🧨4.2二维数组传参
void test(int arr[3][5])//ok?
{}//这是没问题的
void test(int arr[][])//ok?
{}//这个不行
void test(int arr[][5])//ok?
{}//这个可以
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}//传过来是二维数组的数组名是第一行的地址,不能使用一级指针存储,只能使用数组指针
void test(int* arr[5])//ok?
{}//这里面是一个指针数组,传过来的是地址,应该用指针接收,所以错误
void test(int (*arr)[5])//ok?
{}//这个可以
void test(int **arr)//ok?
{}//传过来是第一行地址,是int类型的地址,二级指针只能接收int*类型的地址,所以不可以
int main()
{
int arr[3][5] = {0};
test(arr);
}
💞4.3一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(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;
}
千万参数里面不能写成二级指针,我传的一级指针,不是在一级指针的地址。
🎞️4.4二级指针的传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
int *arr[10];
test(arr)//arr是首元素的地址,因为这个数组里面的每个元素的类型是int*的,所以用二级指针接收时没问题的
test(pp);
test(&p);
return 0;
}
说明 pp=&p *pp=p **pp=*p
我们掌握好参数的传参,相信大家对我刚才所说的知识点都应该有了一个更好的理解,我们接下来再来学习一个新的知识点
🎟️五、函数指针
❤️🩹5.1函数指针的定义
函数指针就是指向函数的指针,把函数的地址交给一个指针,就构成了函数指针
那我们怎么去定义函数指针呢??
还是老问题,我们需要知道函数的类型,才能作为函数的类型,比如
int add(int x,int y);这个函数的类型是 int (int,int);去掉函数名就是函数的类型,我们来看两种定义:
int add(int x,int y)
{
return x+y;
}
//定义1
int (*p1)(int,int);
//定义2
int *p1(int,int);
答案是定义1是正确的,意思是先和p1结合,说明是一个指针,这个指针指向的类型是int (int,int);
但是定义2是一个函数的声明,这个函数有两个参数,第一个参数为int,第二个参数也为int,返回类型为int.
注意,不要写成这样:int (int,int) *p,这样写是错误的,这个等会在typedef的时候也是相同的写法。并参数里面的变量可写可不写
我们知道了怎么定义了,那我们怎么使用它呢??
int add(int x,int y)
{
return x+y;
}
int (*p1)(int,int)=&add;
这么写肯定没有问题,把add函数的地址取出来放到指针里面。意思就是p1是一个函数指针,指向的函数有两个参数,一个为int,一个也为int,返回类型为int.
那我们再来看一个代码
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
我们看到函数名和取地址函数名是相等的,指针里面存放的是指向函数的地址,既然取地址函数名的地址和函数名的地址是一样,那我们可不可以这么去使用??
int (*p1)(int,int)=add;
答案是可以,我们把地址放到指针里面,主要是想用指针来操作函数,那这两种方法放到地址都是一样的,那么进行指针操作的效果也应该是一样的,那我们具体来看看怎么使用函数指针来操作函数吧
🎀5.2函数指针的使用
int add(int x,int y)
{
return x+y;
}
int main()
{
//第一种
int (*p1)(int,int)=&add;
int sum=(*p1)(2,3);
//第二种
int (*p1)(int,int)=&add;
int sum=p1(2,3);
printf("%d",sum);
return 0;
}
第一种我们先进行解引用p1找到函数的内容在进行操作,相当于p=add,也就是(*p)(2,3)=add(2,3);
第二种那本身p就是&add,&add=add,那么p=add,所以p(2,3)=add(2,3),
所以星号和取地址操作符在这就是一个摆设,加星号和取地址更符合我们之前的理解,在函数指针里面,*可以不加,如果要加,必须带括号,不然就会出现问题例如:*p(2,3)这个先调用然后返回一个整数,你在对整数解引用会发生错误。
🎗️5.3通过有趣的代码来更好的理解函数指针
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
大家可以对着图和自己的理解去在好好的分析这两个代码想表达的意思,
我们看到代码2它复杂了我们如何进行优化呢??
typedef void(*)(int) pfun_t;
常规是这样重命名的,但是在这里不行,必须写成下面的这种形式,数组指针的重命名也是和下面的一样。
typedef void(*pfun_t)(int);
我们在来看这个代码是不是更好的理解了呢??
pfun_t signal(int, pfun_t);
我们接下来讲新的知识点
🎫六、函数指针数组
🏀6.1函数指针数组的定义
我们在之前学过指针数组例如intarr[10],chararr[10]。其实他们准确来说叫整型指针数组,或叫字符指针数组,是指向存放整型指针的数组,或叫存放字符指针的数组,那我们来类比函数指针数组,就是存放函数指针的数组,函数指针就是存放数组的地址,所以意思就是把多个函数的地址放在一个数组里面。我们定义一个数组,数组里面假设5个元素,类型为里面元素的类型,即为函数指针类型,那让我们来看看它具体是怎么定义的吧
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
第二个啥也不是,大家自己分析,第三个表示有问题,必须放在括号里面和星号离得近才行。
🌭6.2用法即用途
我们大致知道了函数指针数组的定义了那我们怎么去使用呢??
常见的用途:转移表,
我们来举一个例子,我们来写一个只能实现加减乘除的计算器吧:
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
switch (input)
{
case 1:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = add(x, y);
printf( "ret = %d\n", ret);
break;
case 2:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = sub(x, y);
printf( "ret = %d\n", ret);
break;
case 3:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = mul(x, y);
printf( "ret = %d\n", ret);
break;
case 4:
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = div(x, y);
printf( "ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
breark;
default:
printf( "选择错误\n" );
break;
}
} while (input);
return 0;
}
我们可以通过case语句来分别调用每个函数,但是我们有没有想过一个问题,这个计算器只有四个功能,我们正常的计算器有许多功能,所以我们在添加更多的功能的时候,我们需要增加更多的case语句,这样使得代码会变得特别长,那我们用case语句·实现的思想也就是通过操作数来实现,操作数是连续,那我们可以不可以把操作数想象成数组下标,然后访问数组里面的元素(即为对应的函数),那我们来改造代码。
我们写一个函数指针数组,来存放函数指针。
int(*p[5])(int,int)={NULL,add,sub,mui,div};
通过访问下标来访问里面的元素,例如 p[1](2,3);就是调用假发这个函数。以此类推。
改造后的代码为:
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0;
}
后来我们要添加其他相同参数和返回类型的函数时候,就可以直接往数组里面加元素,使代码量还是这么多,这个方法也叫转移表。希望大家能了解,并且能用它写出好的代码才是最重要的。
🍟七、指向函数指针数组的指针
相信大家看到这个标题的时候已经茫然了,不要着急,我就是来给你解决这种茫然的,我们学过数组指针,就是把数组的地址用指针存放起来,那么,函数指针数组是不是也叫数组,也使把数组的地址用指针来存放,我们需要一步步的来写
先是函数指针–函数指针数组–函数指针数组的指针。
void test(const char* str)
{
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
我们通过这个图可以很快的知道了怎么通过函数指针数组来写指向函数指针数组的指针
其实这些东西理解起来不难,我们只要弄清楚到底是是和星号结合还是[]这个操作符结合,就能知道是指针还是数组了,我们一定要从最基础的往深的方向的分析,不能直接一上来就开始分析,往往把自己弄糊涂了,还希望读者可以自己去理解和分析,等我们把弄懂了,博主在出一期关于这个知识点题目的博客,方便大家去练习。
🍔 八、总结
今天终于把关于指针的所有知识点讲完了,当我们理解好这些知识点的时候,我们会发现,我们对于指针的问题会变得不在害怕,因为我们不在感到陌生,所以我也希望各位读者能通过我的博客真真实实的学到东西,并且也希望各位友友给我点点赞,让更多的人能通过这篇博客受益匪浅,那我们今天就讲到这里了,我们下篇再见!!