本专栏目的
- 更新C/C++的基础语法,包括C++的一些新特性
前言
- 指针是C/C++的灵魂,和内存地址相关联,运行的时候速度快,但是同时也有很多细节和规范要注意的,毕竟内存泄漏是很恐怖的
- 指针打算分三篇文章进行讲解,本专题是三,完结篇,介绍了指针做函数参数,函数指针,回调函数,左右法则解决复杂指针等问题
- 专题一:指针三大专题详解1(包含常见错误,代码均可运行)
- 专题二:指针三大专题详解2(指针与数组关系,动态内存分配,代码均可)
- 制作不易,欢迎收藏+点赞+关注,本人会持续更新
文章目录
指针高级
指针做函数参数
学习函数的时候,讲了函数的参数都是值拷贝,在函数里面改变形参的值,实参并不会发生改变。
如果想要通过形参改变实参的值,就需要传入指针了。
注意:虽然指针能在函数里面改变实参的值,但是函数传参还是值拷贝。不过指针虽然是值拷贝,但是却指向的同一片内存空间。
指针做函数返回值
返回指针的函数,也叫作指针函数。
和普通函数一样,只是返回值类型不同而已,先看一下下面这个函数,非常熟悉对不!
int fun(int x,int y);
接下来看另外一个函数声明
int* fun(int x,int y);
这样一对比,发现所谓的指针函数也没什么特别的。
注意:
不要
返回临时变量
的地址可以
返回动态申请的空间
的地址可以返回静态变量和全局变量的地址
函数指针
如果在程序中定义了一个函数,那么在运行时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数指针定义
函数返回值类型 (* 指针变量名) (函数参数列表);
-
“函数返回值类型”表示该指针变量所指向函数的 返回值类型;
-
“函数参数列表”表示该指针变量所指向函数的参数列表。
那么怎么判断一个指针变量是指向变量的指针,还是指向函数的指针变量呢?
-
看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。
-
函数指针
没有++和 --运算
函数指针使用
定义一个实现两个数相加的函数。
int add(int a,int b)
{
return a+b;
}
int main()
{
int (*pfun)(int,int) = add;
int res = pfun(5,3);
printf("res:%d\n",res);
return 0;
}
在给函数指针pfun赋值时,可以直接用add赋值,也可以用&add赋值,效果是一样的。
在使用函数指针时,同样也有两种方式,1,pfun(5,3); 2,(*pfun)(5,3)
案例
计算器
用函数指针实现一个简单的计算器,支持+、-、*、/、%
//plus sub multi divide mod //加 减 乘 除 取余
当功能太多时,switch语句太长,因此不是一种好的编程风格。好的设计理念应该是把具体的操作和和选择操作的代码分开。
函数指针作为转换表
转换表就是一个函数指针数组。
#include<stdio.h>
#include<math.h>
// 转换表
// 转换表 step1:
//(1.1)声明 转台转移函数
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
double hypotenuse(double, double);
//(1.2)声明并初始化一个函数指针数组 pfunc:数组 数组元素:函数指针 返回值:double型数据
double(*pfunc[])(double, double) = { add, sub, mul, div, hypotenuse };//5个转移状态
//状态转移函数的实现
double add(double a, double b){ return a + b;}
double sub(double a, double b){ return a - b; }
double mul(double a, double b){ return a * b; }
double div(double a, double b){ return a / b; }
double hypotenuse(double a, double b){ return sqrt(pow(a, 2) + pow(b, 2)); }
void test()
{
//转换表 step2:调用 函数指针数组
int n = sizeof(pfunc) / sizeof(pfunc[0]);//转移表中 包含的元素个数(状态转移函数个数)
for (int i = 0; i < n; ++i){
printf("%.2lf\n",pfunc[i](3, 4));
}
}
int main()
{
test();
return 0;
}
typedef
一,使用typedef为现有类型创建别名,给变量定义一个易于记忆且意义明确的新名字。
- 类型过长,用typedef可以简化一下
typedef unsigned int UInt32
- 还可以定义数组类型
typedef int IntArray[10];
IntArray arr; //相当于int arr[10]
二、使用typedef简化一些比较复杂的类型声明。
例如:
typedef int (*CompareCallBack)(int,int);
上述声明引入了PFUN类型作为函数指针的同义字,该函数有两个类型分别为int、int、char参数,以及一个类型为int的返回值。通常,当某个函数的参数是一个回调函数时,可能会用到typedef简化声明。
例如,承接上面的示例,我们再列举下列示例:
int callBackTest(int a,int b,CompareCallBack cmp);
callBackTest函数的参数有一个CompareCallBack类型的回调函数。在这个示例中,如果不用typedef,callBackTest函数声明如下:
int callBackTest(int a,int b,int (*cmp)(int,int));
从上面两条函数声明可以看出,不使用typedef的情况下,callBackTest函数的声明复杂得多,不利于代码的理解,并且增加的出错风险。
所以,在某些复杂的类型声明中,使用typedef进行声明的简化是很有必要的。
回调函数
首先要明确的一点是,函数也**可以作为函数的参数来传递。**当做函数参数传入的函数,称之为 回调函数。
挑战:实现一个与类型无关的查找函数。下期将公布答案。
如何看懂复杂的指针
指针大家都学过了,简单的指针相信大家都不放在眼里,就不再赘述,但是复杂的你能理解吗?能理解指针就学的差不多了,至于如何运用只要你看懂指针就知道应该给它赋什么值,怎么用。
- 首先咱们一起来看看这个:
int (*fun)(int *p)
- 首先需要分析这个是不是一个指针,如果是,是什么指针?如果不是,那是什么?
-
- 根据(*fun)可知,fun是一个指针
- 然后看fun的后面是一个函数参数列表,可以确定是一个指向函数的指针
- 指向的函数的返回值是什么类型呢,再回头看看最前面发现是一个int
- 最后我们可以根据这个函数指针写出对应的函数
结果如下:
int foo(int *p)
{
reutrn 0;
}
右左法则
上面我们分析了一个函数指针,那结果是如何得出来的呢?全靠经验吗,NO,其实是有方法的。
这个方法叫做右左法则:
-
右左法则不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的。
-
右左法则使用:
-
- 首先从最里面的圆括号(应该是标识符)看起,然后往右看,再往左看;
- 每当遇到圆括号时,就应该调转阅读方向;
- 一旦解析完圆括号里面所有东西,就跳出圆括号;
- 重复这个过程知道整个声明解析完毕。
-
案例走起
1.int (*p[5])(int*)
分析:
- 首先看p,p结合[]形成数组,然后往左看,发现数组里面存储的是指针,即
指针数组
- 然后在跳出(),向右看,看到int*,说明这是一个函数指针参数列表,表示
指针数组里面存储的是函数指针
- 跳出圆括号,往左看,发现函数指针的
返回类型是int
- 案例如下:
int func1(int* a);
int func2(int* a);
int func3(int* a);
int func4(int* a);
int func5(int* a);
int (*p[5])(int*) = {func1, func2, func3, func4, func5};
2. int (*fun)(int *p,int (*pf)(int *))
解析:
- fun与*结合形成指针;
- 往后看是一个参数列表,说明是一个
函数指针
,但是参数里面还有一个函数指针(也称为回调函数
) - 往前看可以
确定函数指针的返回类型
。 - 案例如下:
int p(int* a);
int f(int* a, int (*pf)(int *));
int (*func)(int*, int (*pf)(int*)) = f;
3. int (*(*fun)[5])(int *p)
解析:
- fun与*结合,形成指针;
- 往后看发现了一个[5]说明是一个
指向数组的指针
,注意与案例1的区别。 - 再往前看,发现有一个*,说明
数组里面存的是指针
,即fun是指针数组。
- 跳出圆括号往后看,发现了参数列表,说明
数组里面存的是函数指针
; - 再往前看可以确定
函数指针的返回类型
。 - 案例如下:
int aa(int* p)
{
……
}
void main()
{
int (*func[5])(int*)
}
4. int (*(*fun)(int *p))[5]
解析:
-
fun与*结合,形成指针;
-
往后看发现了参数列表,
说明fun是一个函数指针
; -
往前看遇到了*说明,函数指针的
返回类型是一个指针
,是什么指针继续往后解析; -
往后看发现了[5] 说明是一个数组指针,最前面一个int,说明fun这个函数指针的返回类型是一个数组的指针
类型为int (*)[5]
-
案例如下:
int* func1(int *p);
int* func2(int *p);
int* func3(int *p);
int* func4(int *p);
int* func5(int *p);
int (*(*func(int* p)))[5] = {func1, func2, func3, func4, func5};
// 其实这个等价于
int* (*fun[5])(int *p);
// typedef的写法
typedef int (*FUNC)[5];
typedef FUNC (*FUNC1)(int *);
技巧:typedef写的时候,运用整体思想,一层一层解析。
5. int(*(*fun())())()
解析:
- fun与()结合,
说明fun是一个函数
. - 往前看发现了一个*,说明函数返回类型为指针,
什么指针呢?
- 往后看发现了参数列表,fun函数返回的是一个函数指针,那这个函数指针的返回类型是什么呢?
- 往前看又发现了一个*,说明函数指针返回类型也是一个指针,那这个指针是什么指针呢?
- 往后看又发现了一个参数列表,说明是个函数指针,往前看这个函数指针返回的是int类型。
- 案例如下:
typedef int (*FUNC)();
typedef FUNC (*FUNC1)(); // 这个时候,还剩下 (*fun())()
typedef FUNC1 (*FUNC2)(); // 这个时候,还剩下 *func()
// 技巧:typedef写的时候,运用整体思想,一层一层解析。
当然这个案例实际中是不会有人使用的,太复杂了,不容易读,但是理解指针还是很好的
总结
实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。**应该用typedef来对声明逐层分解,增强可读性,**提示,在C++的时候增加了应用类型,使得指针更容易读。
指针变量有两种类型
指针变量的类型和指针所指向的对象(如函数指针)的类型
指针变量的类型
只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。
-
int* ptr; //指针的类型是int
-
char* ptr; //指针的类型是char
-
int** ptr; //指针的类型是int**
-
int(*ptr)[3]; //指针的类型是int()[3]
-
int*(*ptr)[4]; //指针的类型是int*(*)[4]
指针变量指向的对象的类型
- 你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。
- int*ptr; //指针所指向的类型是int
- char*ptr; //指针所指向的的类型是char
- int**ptr; //指针所指向的的类型是int*
- int(*ptr)[3]; //指针所指向的的类型是int()[3]
- int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
注意事项:
-
指针变量也是变量,也有存储空间,存的是别的变量的地址。
-
要注意指针的值,和指向的对象的值得区别,如:int** 与 int* 的区别
-
普通变量中的内存空间存放的是,数值或字符等。 ----直接存取
-
指针变量中的内存空间存放的是,另外一个普通变量的地址。----间接存取
-
-
连续定义多个指针变量时,容易犯错误,比如:int *p,p1; 只有p是指针变量,p1是整型变量
-
避免使用为初始化的指针,很多运行错误都是由于这个原因导致的,而且这种错误又不能被编译器检查所以很难被发现,解决方法:初始化为NULL,报错就能很快找到原因,养成习惯。
-
指针赋值时一定要保证类型匹配,由于指针类型确定指针所指向对象的类型,操作指针是才能知道按什么类型去操作
-
在用动态分配完内存之后一定要判断是否分配成功,分配成功后才能使用。
-
在使用完之后一定要释放,释放后必须把指针置为NULL
的值得区别,如:int** 与 int* 的区别
-
普通变量中的内存空间存放的是,数值或字符等。 ----直接存取
-
指针变量中的内存空间存放的是,另外一个普通变量的地址。----间接存取
-
连续定义多个指针变量时,容易犯错误,比如:int *p,p1; 只有p是指针变量,p1是整型变量
-
避免使用为初始化的指针,很多运行错误都是由于这个原因导致的,而且这种错误又不能被编译器检查所以很难被发现,解决方法:初始化为NULL,报错就能很快找到原因,养成习惯。
-
指针赋值时一定要保证类型匹配,由于指针类型确定指针所指向对象的类型,操作指针是才能知道按什么类型去操作
-
在用动态分配完内存之后一定要判断是否分配成功,分配成功后才能使用。
-
在使用完之后一定要释放,释放后必须把指针置为NULL