指针续集来咯:深入了解指针(3)

文章目录

1. 字符指针变量
2. 数组指针变量
3. ⼆维数组传参的本质
4. 函数指针变量
5. 函数指针数组
6. 转移表


1. 字符指针变量

指针类型中,有一种字符指针char*,通常使用方式如下:

指针续集来咯:深入了解指针(3)_第1张图片

还有另外一种方式如下:

指针续集来咯:深入了解指针(3)_第2张图片

代码 const char* pstr = "hello bit."; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。

指针续集来咯:深入了解指针(3)_第3张图片

上⾯代码的意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。

现在我们练习几道题目:

指针续集来咯:深入了解指针(3)_第4张图片

指针续集来咯:深入了解指针(3)_第5张图片

这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。


2. 数组指针变量

2.1 数组指针变量是什么?

在深入了解指针(2)中,我们学习了指针数组,指针数组本质上是一个数组,数组存放的是地址(指针变量)。通过类比指针数组,我们可以知道数组指针变量其实是一个指针变量。

我们已经熟悉:

  • 整型指针变量:int* pint;存放的是整型变量的地址,能够指向整形数据的指针。
  • 浮点型指针变量:float * pf;存放浮点型变量的指针,能够指向浮点型数据的指针

那么数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

下⾯代码哪个是数组指针变量?

int *p1[10];
int (*p2)[10];

仔细思考,p1和p2分别是什么?

答案如下:

 int (*p)[10];

p先和*结合,说明p是一个指针变量,然后再看括号外面指向的是一个大小为10个整型的数组,所以p是一个指针,指向一个数组,叫数组指针

注意(!!!):[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。

2.2 数组指针变量怎么初始化

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址?就是我们之前学习的 &数组名 。

int arr[10] = {0};
&arr;//得到的就是数组的地址

如果要存放个数组的地址,就得存放在数组指针变量中,如下:

int(*p)[10] = &arr;

指针续集来咯:深入了解指针(3)_第6张图片

调试的时候,我们可以看到&arr和p的类型是完全一致的。

int  (*p)  [10] = &arr;
|     |     |
|     |     |
|     |     p指向数组的元素个数
|     p是数组指针变量名
p指向的数组的元素类型


3. 二维数组传参的本质

有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:

指针续集来咯:深入了解指针(3)_第7张图片

这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

如下图:

指针续集来咯:深入了解指针(3)_第8张图片

所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类
型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:

指针续集来咯:深入了解指针(3)_第9张图片

总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。


4. 函数指针变量

4.1 函数指针变量的创建  

什么是函数指针变量呢?
根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。那么函数是否有地址呢?
我们做个测试:

指针续集来咯:深入了解指针(3)_第10张图片

输出的结果如下:

指针续集来咯:深入了解指针(3)_第11张图片

确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针⾮常类似。如下:

指针续集来咯:深入了解指针(3)_第12张图片

函数指针类型解析:

int      (*pf3)   (int x, int y)
|          |      ------------
|          |            |
|          |            pf3指向函数的参数类型和个数的交代
|          函数指针变量名
pf3指向函数的返回类型

int (*) (int x, int y) //pf3函数指针变量的类型

4.2 函数指针变量的使用 

通过函数指针调⽤指针指向的函数。

指针续集来咯:深入了解指针(3)_第13张图片

输出的结果:

指针续集来咯:深入了解指针(3)_第14张图片

4.3两端有趣的代码 

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);
	

可以尝试分析这两段代码是什么类型,难度较大。

分析过程:

  • 代码1:首先看0前面的括号,类比于int a = (int)3.14,这里是将3.14浮点型数据强制转换成整型类型的数据,所以在数值之前加上类型,便是强制类型转换。这里0的前面括号内不是一个函数指针,返回类型是void,参数为空,将0这个整型类型转换成函数指针类型,变成了一个地址。外面的括号,便是函数调用,可以把(*(void (*)()0)和外面的括号()分成两个部分,第一个部分是函数名,函数名本质上是一个地址,而第二个部分是函数内部的参数,两个部分组合在一起就是一个函数的调用。
        (*  (void (*)())0  )         ();
              ---------               | 
                  |                   |
                  |                   |
       将整型类型强制转换成函数指针      ||
     ----------------------------     |
                  |                   |
                  |             函数的参数部分,参数为空
                  |
    这是一个函数地址调用,相当于一个函数名
  • 代码2:第二个代码中*signal后再加一个括号,表明这是一个函数,返回类型第一个是整形,第二个是函数指针,指向函数的参数是int,返回类型是void。既然已经有参数,该函数的返回类型是是什么呢?就要观察括号外面,我们先把(*signal(in, void(*)(int))看作一个整体,这就是个函数加参数部分,外面便是返回类型那么void()(int)也是一个函数指针,指向的函数参数为int类型,返回类型是void。所以代码2是一个函数声明
 void (  *signal  (int,  void(*)(int))  )(int);
            |       ------------------
            |              |
          函数名           ||
                      函数参数部分

将  void () (int)剥离出来,便是一个函数指针,是原代码的返回类型

4.4 typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。                                                         ⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:

typedef unsigned int uint;
//将unsigned int 重命名为uint

如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:

typedef int* ptr_t;

但是对于数组指针和函数指针稍微有点区别,⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:

typedef int(*parr_t)[5]; //新的类型名必须在*的右边

函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

那么要简化代码2,可以这样写:

//代码2
void (*signal(int, void(*)(int)))(int);
//简化后
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);


5. 函数指针数组

数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到⼀个数组中,这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];

答案是:parr1。parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。


6. 转移表

函数指针数组的⽤途:转移表。比如,计算器的⼀般实现。

先编写一个初始菜单页面,封装在menu函数内部,然后其余四个函数,分别对应加减乘除。

指针续集来咯:深入了解指针(3)_第15张图片

然后便是主体部分,do-while循环和switch条件语句,判断条件用input,当你输入数字,与菜单相应功能匹配。

指针续集来咯:深入了解指针(3)_第16张图片

我们可以观察到,switch内部有许多相同的代码,造成了代码冗余,那如何进行改进呢?这时候就要用到函数指针了。创建一个函数指针数组,将四个函数放进里面。前面多加一个0,因为初始菜单menu中,各个函数对应数字从1开始,然而数组下标索引从0开始,所以添加一个元素,使数组对应的下标与函数在菜单中对应的数字对应。此时代码看起来简洁很多。

指针续集来咯:深入了解指针(3)_第17张图片

还可以使用回调函数,在函数内部调用函数。

指针续集来咯:深入了解指针(3)_第18张图片

此时,可能有人疑惑,虽然看起来代码少了一些,但是这样操作好像也没有大幅度提升写代码的效率。那是因为我们这里的调用函数才四个,如果一个很复杂的系统,有一百个甚至上千个函数,统一用转移表来调用,就不用把相同的代码写上几百遍,节省时间。


总结

这篇深入了解指针(3),慢慢在帮助我们提高对指针的理解并加强指针的应用。指针是C语言的灵魂,所以更加需要我们多多敲代码,运行并调试,理解背后的逻辑。多多重复,百炼成钢!

创作不易,如果喜欢这篇文章的话,请留下你的三连哦,你们的支持是我最大的动力!!!

指针续集来咯:深入了解指针(3)_第19张图片

你可能感兴趣的:(c语言)