C语言:指针从进阶到精通
1. 字符指针变量
字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char*,我们一般这样使用:
1 | int main() |
这里的两段代码很多人以为是吧字符串hello world放在了cp里面,但是事实是将字符串首字母的地址放在cp里面。
《🗡指offer》里面有一道题:
1 |
|
这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
2. 数组指针变量
数组指针变量是什么?
之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。
数组指针变量是指针变量?还是数组?
答案是:指针变量。
我们已经熟悉:
• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。
• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
1 | int *p1[10]; |
答案是第二个
解释:p先和结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针。
这⾥要注意:[]的优先级要⾼于号的,所以必须加上()来保证p先和*结合。
数组指针变量怎么初始化
数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的 &数组名。
1 | int arr[10] = {0}; |
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
1 | int(*p)[10] = &arr; |
[10]代表指向数组的元素个数
p是数组指针变量名
int为p指向的数组的元素类型
3. ⼆维数组传参的本质
⼆维数组传参的本质
有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
1 |
|
这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维
数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
如下图:
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类
型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:
1 |
|
4. 函数指针变量
函数指针变量的创建
根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
那么函数是否有地址呢?
我们做个测试:
1 |
|
输出结果:
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针⾮常类似。如下:
1 | void test() |
函数指针类型解析:
1 | int (*p)(int,int); |
第一个int是p指向的函数的返回类型。
p是函数指针变量。
后面两个int是指这个函数两个int类型的参数。
函数指针变量的使用
可以使用函数指针来调用其指向的函数
1 |
|
这里有《C陷阱和缺陷》里面的两句代码,我们来解析一下
代码Ⅰ
1 | (*(void (*)())0)(); |
这里首先将整型 0 强制类型转化为void(*)()类型(这是一个函数指针,指向的函数返回值为void,参数也为void)。
然后外面有一个解引用符号*将里面的函数指针解引用,由于这个函数无参,那么后面的括号里面就没有放参数。
最终这句代码的意思就是调用0x00000000地址处的函数。
当然,0x00000000地址处肯定是没有函数的。
代码Ⅱ
1 | void (*signal(int , void(*)(int)))(int); |
这里我们可以看到有一个名为signal的函数,它有两个参数一个为int,另一个是一个void(*)(int)类型的函数指针
一个函数除了名字,参数还缺一个返回类型。将中间的去掉:void (……)(int) 这居然也是一个函数指针!说明signal函数的返回类型是一个void()(int)类型的函数指针。
这是一个函数声明。
也就是说这个函数接收一个整型和一个函数的地址,并返回另一个函数的地址
typedef关键字
typedef是⽤来类型重命名的,可以将复杂的类型,简单化。
⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:
1 | typedef unsigned int uint; |
如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写:
1 | typedef int* ptr_t; |
但是对于数组指针和函数指针稍微有点区别:
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
1 | typedef int(*parr_t)[5]; //新的类型名必须在*的右边 |
函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:
1 | typedef void(*pfun_t)(int);//新的类型名必须在*的右边 |
那么要简化代码Ⅱ,可以这样写:
1 | typedef void(*pfun_t)(int); |
5. 函数指针数组
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,
⽐如:
1 | int *arr[10]; |
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
1 | int (*parr1[3])(); |
答案是:parr1
parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢?
就是 int (*)() 类型的函数指针。
6. 转移表
这里可以参考我的这两篇bolg,第一个是用普通函数调用实现的计算器,第二个是用的转移表(将每个函数的地址放进一个函数指针数组里面)写的计算器,大大减少了重复的代码。
本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!