目录
1.二级指针
我们说,指针其实就是一个存放一个变量地址的变量,那么,指针的本质其实还是一个变量。既然指针是一个变量,那么这个变量就应该有属于它的地址,既然有一个存放它的地址,那么我们也可以套娃把这个地址放在另外应该指针;里面,那么我们就称这个存放指针地址的另外一个指针为二级指针。
我们可以使用一个生动形象的图来表示这样的关系:
这样是不是更好理解了呢?
指针,无非就是一种专门储存地址的变量,再用另外一个指针储存这个变量,就构成了二级指针。我们下面看一下指针的初始化结构:
int a = 10 ;//创建一个变量a
int * p = &a ;//创建一个一级指针p储存a的地址
int * * pp = &p ;//创建一个二级指针储存一级指针的地址
我们在前面讲过,在指针这部分,使用类比推理法是一个很有力的工具,我们再使用类比推理法来解析一下创建二级指针的结构:
这就是一个二级指针,对于二级指针的运算其实和一级指针是互通的,也就是通过解引用操作符*回到上一级,通过取地址操作符取得地址,*和& 仍然可以相互抵消。比如:我们使用**pp其实就是得到了a的值,因为这里*pp表示通过解引用操作符找到一级指针p,然后*(*pp)就是通过对这个一级指针p解引用得到a的值。
在这里我们就可以这样使用这种二级指针两次解引用看一下能不能打印出变量的值:
在这里我们可以清楚的看到 a == *p == **pp == 10
2.指针数组
2.1指针数组的定义和表现形式
我们在之前学过数组,数组里面每个元素都是整形的叫做整形数组,数组里面每个元素都是浮点数的叫做叫做浮点数数组,那么一个数组里每个元素都是指针的就是指针数组。并且,每个指针都应该是一个储存地址的变量单元格,所以指针数组本质上其实是每个元素里面都储存了一个指针(地址变量)。
这样我们其实就可以使用一个指针指向几个指针构成一个指针数组:
int a = 10;
int* pa = &a;
int b = 20;
int* pb = &b;
int c = 30;
int* pc = &c;
int d = 40;
int* pd = &d;
int* arr[4] = { pa,pb,pc,pd };
//或者我们可以使用另外一种表现形式
//int* arr[4] = { &a, &b, &c, &d };
要理解这个指针数组其实也不难,指针数组本质其实就是数组,有整形(int)数组,有浮点型(double)数组,有字符型(char)数组,也就可以有指针数组。当然,指针数组是要细分的,指针数组还可以分为:整形指针(int*)数组,浮点型指针(double*)数组,字符型指针(char*)数组......
既然一个指针数组可以存放多个地址,那么我们其实也可以存放一个多个数组的首元素地址,前面我们又知道:一个数组的首元素地址我们通常使用这个数组名来代替,那么我们就可以写出下面的代码:
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 1,5,8,9,5 };
int arr3[5] = { 3,6,8,5,1 };
int arr4[5] = { 3,8,7,6,4 };
int arr5[5] = { 7,3,5,6,4 };
int* parr[5] = { arr1,arr2,arr3,arr4,arr5 };
这样就是指针数组的两种表现形式,下面的这个看起来的话其实是有点像二维数组,一会儿我们就会讲到,其实用指针数组储存的这几个数组的长度可以各不相同,这也是使用二维数组无法实现的一个功能,但是,数组长度各不相同我们就要考虑数组越界的问题了。
这样,其实各不相同的所谓“二维数组”的存在其实就非常抽象,因为它似乎起不到什么作用。但其实,我们总有一天写一些比较特别的代码的时候,我们就会用到这样“奇怪”的代码,学会这样的表现形式,会让我们在后面的学习过程中更加得心应手,我们可以运用更加广泛的思维去写出代码,没准现在你看起来“没什么用”的代码形式会帮你一个大忙!
2.2指针数组模拟实现二维数组
2.2.1二维数组
在我们学习这个指针数组模拟实现二维数组之前,我们先回顾一下二维数组的定义:
数组的元素都是内置类型的,如果我们把⼀维数组做为数组的元 素,这时候就是⼆维数组,⼆维数组作为数组元素的数组被称为三维数组,⼆维数组以上的数组统称 为多维数组。
给定一个二维数组arr[3][5]就表示一个数组有3行5列,也就是说:前面的arr[3]表示这个数组有3行,后面的[5]表示这个数组有5列,准确的说,一个二维数组是按照行“分割”的:
像这样,我们把这个3行5列的二维数组按照行“分割”,其实就可以看成3部分:arr[0]、arr[1]、arr[2],这同时也是这个数组的行号,然后就是这三部分每个部分都有一个“头”,这就是每行的首元素。
比如我们需要找到第2行第4列的元素,我们就要先通过行号找到arr[1]也就是第二行,然后就是第4列元素,我们就得到了arr[1][3],这里面存储的就是我们想要的元素。
或者我们从地址的方向理解的话,其实数组无非就是一系列连续存放的元素罢了,这里我们使用一段代码打印一下一个3行5列的每个元素的代码:
在这里我们可以看到:二维数组确实是连续存放的,并且以行作为分割 标志,一一存放数组中的每一个元素。所以我可以用下面的图示表示一段二维数组:
这样就可以想象出一个二维数组的大概形式了,我们的计算机想要找到一个二维数组的元素时,先通过寻找行找到这个行首元素的地址,然后再通过列定位到这个行里面的元素,然后实现二维数组元素的调用。
2.2.2使用指针数组模拟实现二维数组
通过上面我们对二维数组的回顾,我们也大概了解了一个二维数组的构成,接下来我们可以使用指针数组来模拟实现这个二维数组。需要注意的是,我们是使用指针数组模拟实现一个二维数组,这个模拟实现的数组其实从根本上来说并不是一个二维数组。
那么我们就可以先列一下我们的思路:
首先我们需要实现一个模拟的二维数组,我们就需要通过定义几个分开的区域表现行的特性,这里我们用几个相应的一维数组来实现,然后再通过这个数组的列找到我们想要的元素,那么,其实我们就可以列出下面的实现原理图:
这里,parr就是一个指针数组,里面储存着arr1,arr2,arr3,也就是这三个数组的首元素地址,然后arr1,arr2,arr3其实模拟的就是一个二维数组的1、2、3行,这样我们就得到了这个数组的行和列,写一下代码:
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* parr[3] = {arr1,arr2,arr3};
我们就通过一段指针数组模拟实现了一个二维数组,之所以是模拟实现,其实就是这几个一维数组之间其实并不是连续存放的,它们直接有一定的空隙,所以这个模拟实现的二维数组并不能等同于二维数组。
我们想要通过这个模拟实现的二维数组找到第2行第3列的元素就可以使用p[1]首先找到这个对应“行”的首元素的地址,然后就是找到这个对应“列”的地址。
我们在前面对数组曾经剖析过这样的结论:一个arr数组配上[ k ] 时,其实就是通过首元素地址找到了第k个元素,这里计算机会把把 arr[ k ] == *( arr + k ) ,得到arr元素的地址加上相应的元素间隔,得到了我们要求的元素。
在这里我们又需要用到这方面的知识,我们使用parr[ 1 ]就得到了一个地址,这个就是arr2的地址,在我们的模拟二维数组里面就是第二行的首元素地址,在计算机里面就是 *(parr + 1)。我们在想要找到第3列的元素,我们就要通过得到的这个parr[ 1 ]再得到第3列的元素,在计算机里表示就是*(*(parr + 1)+ 2 ),写出来就是parr[1][2],这样我们就得到了模拟实现的二维数组的第2行第3列的元素,用代码实现一下就是:
3.字符指针
我们接触了很多指针,它们的类型都是int *类型的,其实除了int * 型的指针,还有很多种类型的指针,这里我们主要介绍 char * 型的指针。
char * 型指针就是字符指针,字符又分为单字符和字符串,这就是组成我们字符指针变量的两种小分类。
单字符使用:
char ch = 'G';//定义一个字符,储存一个单字符'G'
char * pc = &ch ;//定义一个字符指针,储存上面字符变量ch的地址
字符串使用:
char* pstr = "Hello World";
printf("%s\n", pstr);
这里我们运行一下这个通过字符串指针打印的结果:
很明显的发现,我们打印成功了,但是你觉得这个指针pstr里面储存的是整个字符串的地址还是第一个元素的地址呢?
这里我们就可以使用打印pstr的地址和pstr +1的地址:
这个非常好用的方法很直观的表示了我们pstr其实代表的是字符串里面第一个元素’H‘的地址,假设我们这个指针里面储存的是整个字符串,我们输出的pstr和pstr+1之间的差距就是整个字符串的长度了,所以我们可以推断出pstr其实代表的是字符串里面第一个元素’H‘的地址。
在《剑指offer》这本书里面收录了一段这样的代码:
#include <stdio.h>
int main()
{
char str1[] = "Hello World";
char str2[] = "Hello World";
const char *str3 = "Hello World";
const char *str4 = "Hello World";
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;
}
我们运行一下结果:
我们发现,这里面的str1和str2其实是不一样的地址,而str3和str4其实是同一个地址,我们再使用打印地址验证这个结论:
可以看到,其实str1和str2就是两个内容相同的两个不同的字符串,str3和str4是两个不同的字符指针指向了同一个字符串,这里我们的计算机内存中开辟了三个空间存储同样内容的三个字符串“Hello World”,只有字符串指针可以实现两个指指针指向同一个元素的功能,做个图示理解就是:
2.数组指针
数组指针和指针数组是两个东西,按照语法来说的话这两个名字都该加上一个“的”,也就是变成:数组的指针和指针的数组,前者本质上是一个指针,是一个存放数组的指针,后者本质上是一个数组,是存放指针的数组,这就是区别。
我们要学习数组指针,就要从我们之前学习过的其他指针入手,比如前面学的整形指针(int*)和上面的字符指针(char*),整形指针是储存整形数据的指针,字符指针就是储存字符白变量的指针,所以我们也可以推出数组指针就是储存数组的指针
如果我们拿出下面的代码,让你分辨一下哪个才是数组指针,看一下你能不能分辨出来:
int *p1[10];
int (*p2)[10];
答案其实是:上面的是指针数组,下面的是数组指针,这里我们使用一个图解分析一下:
其实,这就涉及到一个优先结合的顺序问题了。
首先,结合的顺序应该是()>[]>*
1)一般来说一个指针数组int * p [10],应该是p优先和[10]结合,表示了p[10]是一个数组,然后就是int 和 * 结合,表示这个p[10]数组里面每一个元素都是int * 类型的指针。也就是说我们的变量p是一个数组,数组里面每个元素都是一个指针(即地址)。
2)再来看看数组指针 int (*p) [10] ,其实就是优先级被()改变了,我们就要先把*和p结合,这样我们就得到了一个指针*p,然后我们的(*p)和[10]结合,表示了一个数组(*p)[10],前面的int 修饰这个数组,表示我们的数组里面每个元素都是int 类型的。也就是说变量p其实是一个指针,指针指向的是一个有10个元素的数组,数组里面的每个元素是一个int整形。
我们的主体由变量名字p决定,p是什么,我们的这个东西就是什么。
还是不理解的话,我们就可以举例子:
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int (*p)[10]=&arr;
我们可以使用这个数组指针直接打印出这个数组的每一个元素,这就是和指针数组不一样的地方,我们看到前面我们介绍的指针数组是每个元素都是一个地址,它可以用来指向多个数组首元素地址;但是我们这个数组指针更加“专一”,数组指针只可以指向一个数组,然后每个元素都是这个数组的元素并且一一对应。
3.二维数组传参
当我们使用一个二维数组并把它传给一个函数的时候,我们会这样写:
#include <stdio.h>
void test(int arr[3][5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (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[3][5]表示有3行5列,所以我们把这个数组分成3部分,每部分是5个元素。把二维数组的数组名分开来理解,就是arr[3][5]中arr[3]其实就可以看作这个二维数组的数组名,然后这个数组名arr[0],arr[1],arr[2]其实又可以看作是5个一维数组的首元素地址,其实就是三个指针。这三个指针
现在我们有一个一维数组在主函数中,我们需要使用test函数打印出这个arr数组里的全部元素,我们在test函数里输入的参数就要有数组 arr[](注意:这里不能是arr,如果是arr就不能表示一个数组,这样表示的就是一个普通的整形变量),数组长度 len ,在主函数中有一个arr[5],所以我们在主函数传入的参数就是arr,5.这里的arr表示的就是数组的首元素地址(当然也可以用&arr[0]替换)。
#include <stdio.h>
void test(int arr[], int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
test(arr, 5);
return 0;
}
那么我们可以使用 数组作为test函数的形参,我们是不是也可以使用一个指针作为形参呢?答案其实是肯定的。我们的确可以使用一个指针作为形参,而我们传入的参数还可以是这个首元素地址,因为我们test函数里面需要的其实就是这个形参指针里面存储的地址,就可以改成下面这样:
#include <stdio.h>
void test(int *p, int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", p[i]);
}
}
int main()
{
int arr[5] = { 1,2,3,4,5 };
test(arr, 5);
return 0;
}
同理,我们现在使用一个二维数组传参,我们想要在主函数里面定义一个二维数组arr[3][5],我们要使用一个test函数打印出这个二维数组里面的所有元素,我们就要传入一个arr[ ][5]表示数组(注意,这里不能只有一个arr[ ][ ],二维数组传参必须带上每行的元素个数),r表示行,c表示列,我们除了这样传参之外还可以把这个arr[ ][5]前面的arr[ ]用一个指针代替,也就是用指针指向前面的每行首元素,而且每行必须要有5个元素,所以我们就可以使用数组指针,一个数组指针指向第一行,打印完成这行之后再让数组指针指向下一行的首元素地址,再次重复打印操作...这样我们就可以得到打印完的全部元素,下面是实现代码:
#include <stdio.h>
void test(int (*p)[5],int r,int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ",p[i][j]);//这里也可以写成*(*(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;
}
这样我们就只要在主函数传入首行首元素地址就可以得到整个二维数组的全部元素的打印。
所以,二维数组传参,形参部分也可以写出数组形式,也可以写成指针形式。
4.函数指针
4.1函数指针变量的定义和创建
我们知道了数组指针是指向一个数组的指针,那么我们就可以知道函数指针其实就是指向一个函数的指针。
有人可能会疑问:函数也有地址吗?每次其实函数也是有地址的,不信?这里打印一个函数地址看看:
在这里我们可以看到,不仅函数名取地址可以打印出地址,我们就直接使用一个函数名也可以打印出地址。
那么,既然这个函数有地址的话,我们就可以使用一个指针存放这个地址,这就要创建一个指针变量了。和数组指针类似又不同,我们需要使用()把函数指针名放起来,还要再在外面加上函数的格式,比如上面的test函数,我们要把这个test函数的地址放在一个函数指针里面的话,就要像下面这样操作:
void (*p)() = &test;//也可以直接写成test
这样就是创建一个函数指针的格式,我们也可以打印出来这个地址:
其实三者是一样的。
如果换成别的函数又要怎么做呢?这里我们定义一个Add函数用来表示两个数相加,返回值为int,就可以写出下面的代码:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = &Add;//这里也可以写成&Add
printf("%p\n", p);
printf("%p\n", Add);
printf("%p\n", &Add);
return 0;
}
打印一下结果:
再抽象一点:定义一个函数:
void Fuc(int *p,char str,double x);//定义一个函数
那么我们定义的函数指针就可以这样写:
void (*p)(int*,char,double)=&Fuc;//&Fuc也可以写成Fuc
4.2函数指针变量的使用
我们可以通过函数指针来调用我们指针指向的这个函数。
比如我们定义了一个Add函数,我们定义一个函数指针,怎么使用这个函数指针调用这个函数呢?很简单,模仿一下我们使用Add函数的样子就可以了。
正常情况我们不使用函数指针就可以这样使用:
int Add(int x, int y)
{
return x + y;
}
int main()
{
printf("%d", Add(1, 2));
return 0;
}
这样就是简单的一个函数调用和打印。
那么如果使用函数指针怎么使用呢?模仿一下函数调用的格式就好了!
int (*p)(int, int) = &Add;
printf("%d\n", (*p)(1, 2));
然后我们在这里把这个*p(1,2)改成p(1,2)其实也是可行的,二者其实是等价的。
那么我们可以使用这些代码写出下面的代码并运行一下:
这样我们就成功使用函数指针调用了这个函数。
4.3两段有趣的代码
代码1:
(*(void (*)())0)();
代码2:
void (*signal(int , void(*)(int)))(int);
乍一看这两段代码似乎很让人头疼,但其实,只要你好好去分析这两段代码,你会发现其实也没有那么难。
代码1可以理解成这样:
下面这个代码2就简单描述一下:
这里其实可以使用类型重命名来简化这段代码2
4.4typedef关键字
typedefy是用来重命名的关键字,我们可以利用这个关键字将复杂的类型简单化
例如:unsigned int 写起来很麻烦,就可以使用typedefy简化这个unsigned int
//简化前:
unsigned int a = 10 ;
unsigned int b = 20 ;
//...
//简化后:
typedefy unsigned int uint
//把unsigned int重命名为uint
uint a = 10 ;
uint b = 20 ;
例如我们想要重命名一个数组指针类型int (*) [5]重命名为parr_t
//简化前:
int arr[5] = { 1,2,3,4,5 };
int(*p)[5] = &arr;
printf("%d"*p[0]);
//简化后
typedefy int (*parr_t)[5] ;
//简化成parr_t
//表达原来的代码:
int arr[5] = { 1,2,3,4,5 };
parr_t p = &arr;
printf("%d"*p[0]);
把相应东西替换一下就好了
这样我们就实现了代码的简化。在代码量小的时候可能类型重命名很麻烦,但是当代码量增加或者代码复杂性增加的时候,类型重命名就体现出它的优点了
比如我们上面的代码2,我们就可以把它使用类型重命名简化:
//简化前:
void (*signal(int , void(*)(int)))(int);
//简化后:
typedefy void (*pf_t )(int) ;
//把函数指针类型简化成pf_t
//最后表达原来的代码:
pf_t signal(int , pf_t);
这样就可以逻辑清晰地表达我们的复杂的代码了。
5.函数指针数组
函数指针数组听起来有点绕口,其实就是一个函数指针的变化更新,把普通的指针变成了指针数组。函数指针数组其实本质上还是一个数组。
我们回顾一下指针数组:
int * arr[10] ;
//一个数组arr[10]每个元素类型是int *
我们再看一下函数指针:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//声明函数指针
int(*pf)(int,int) = Add;//定义函数指针的时候在等号右边可以使用 &函数名 或 函数名
//通过函数指针调用函数
int ret = (*pf)(2, 3);//调用函数指针的时候函数名可以使用 (*pf) 或 pf
printf("%d\n", ret);
return 0;
}
函数指针的本质其实就是一个指针,这个指针指向的是一个函数,我们定义了这个函数指针之后可以通过函数指针调用这个函数。
同理,我们的函数指针数组其实本质上是一个数组,准确的讲应该是指针数组,这个指针数组指向的是一个函数数组,我们在定义了函数指针数组之后也可以通过指针数组对应的项调用对应项的函数。
也就是说,我们从函数指针的单个指针指向一个函数变成了多个指针(就是指针构成的数组)指向多个函数(就是函数构成的数组)
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Div(int x, int y)
{
return x / y;
}
int Mul(int x, int y)
{
return x * y;
}
int main()
{
//声明函数指针数组
int(*pfArr[4])(int, int) = { Add,Sub,Div,Mul };//可以是pfArr[4]也可以是pfArr[]
//通过函数指针数组调用函数
int ret1 = pfArr[0](6 ,2);
int ret2 = pfArr[1](6, 2);
int ret3 = pfArr[2](6, 2);
int ret4 = pfArr[3](6, 2);
printf("6+2 = %d\n", ret1);
printf("6-2 = %d\n", ret2);
printf("6/2 = %d\n", ret3);
printf("6*2 = %d\n", ret4);
return 0;
}
像上面的这个就是定义了一个函数指针数组pfArr[4] ,指向加、减、乘、除 四个函数,我们通过这个函数指针数组调用各个函数,这样就得到了我们的想要的计算值:
其实使用这样的函数指针数组是一种简化代码很好的方法,同时函数指针数组也可以在一种数据结构——转移表 里面用来优化程序,提高计算效率。