如下:
void swap_1(int num1, int num2) 作为主函数的子函数,被main函数调用;
在子函数内部实现了num1 和 num2的交换(子函数中加打印可看出),但swap_1被调用后就直接释放掉了,其栈空间内存储的temp,num1,num2也同时被释放调了;因此对于main主函数中的变量a和b,并没有起到赋值的作用。
值传递是单向的,无法通过形参(num1,num2)实现对实参(a,b)的修改。
#include
void swap_1(int num1, int num2)
{
int temp;
temp = num1;
num1 = num2;
num2 = temp;
}
int main(int argc, const char *argv[])
{
int a = 3, b = 5;
swap_1(a, b); // 没有交换
printf("a = %d, b = %d\n", a, b);
return 0;
}
/*
* result:
* a = 3, b = 5
*/
注:此处联想到1个函数实现多个返回值的操作:
1)传入数组指针,对多个数组元素操作;(待举例)
2)传入结构体指针,对结构体元组操作;(待举例)
3)形参中增加指针参数,除了函数返回值外,还可以将结果通过指针返回,如下
同样是子函数swap_2,通过操作传入参数的指针,交换了变量a和b的地址,实现了值(空间内容)的交换,即便swap_2被释放调,变量a和变量b的地址实际已经发生了改变。
#include
void swap_2(int *p1, int * p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(int argc, const char *argv[])
{
int a = 3, b = 5;
swap_2(&a, &b); // 交换了
printf("a = %d, b = %d\n", a, b);
return 0;
}
/*
* result:
* a = 5, b = 3
*/
#include
#include
void test_1(char *p)
{
p = malloc(16);//返回堆内存地址
printf("func: %s, line = %d, p = %p\n", __FUNCTION__, __LINE__, p);
}
void test_2(char **p)
{
*p = malloc(16);//返回堆内存地址
printf("func: %s, line = %d, p = %p\n", __FUNCTION__, __LINE__, *p);
}
int main(int argc, const char *argv[])
{
char *p1 = NULL;
char *p2 = NULL;
test_1(p1);
printf("func: %s, line = %d, p = %p\n\n", __FUNCTION__, __LINE__, p1);
test_2(&p2);
printf("func: %s, line = %d, p = %p\n", __FUNCTION__, __LINE__, p2);
return 0;
}
/*
* result:
*func: test_1, line = 6, p = 0x1c94260
*func: main, line = 18, p = (nil)
*func: test_2, line = 11, p = 0x1c95290
*func: main, line = 20, p = 0x1c95290
*/
一般来讲,const a 表示变量a的值不能被修改;
但是变量a被指针p引用后,通过修改p所指向的空间内容,实现了对const 变量a的值的修改
注:不同编译器可能会有差异 !
#include
int main( )
{
int const a = 3;
int *p = (int*)&a ;
*p = 5;
printf("a = %d,\n", a);
return 0;
}
/*
* result:
* a = 5
*/
int buf[10] = {0} ;
buf:1)表示数组名,如sizeof(buf);2)表示数组元素首地址(常量),即&buf[0],类型是int*
buf[0]:表示数组第一个元素,既可以作为右值被读取,也可以作为左值被写入;
&buf[0]:等价于buf的形式二,是一个地址常量(首元素地址、数组首地址),只能作为右值;
&buf:表示是数组首地址,是一个地址常量,只能作为右值;类型是int(*)[10];
#include
int main()
{
int buf[10] = {0} ;
printf("%p\n",buf);
printf("%p\n",&buf);
printf("%p\n",&buf[0]);
return 0;
}
运行结果:
0x7ffefa449a30
0x7ffefa449a30
0x7ffefa449a30
以上结果表明buf的第二种形式,与&buf 和 &buf[0]的值是一样的,都表示首地址;
再来看另一种:
#include
int main()
{
int buf[10] = {0} ;
printf("%p\n",buf);
printf("%p\n",&buf);
printf("%p\n",&buf[0]);
printf("%p\n",buf+1);
printf("%p\n",&buf+1);
printf("%p\n",&buf[0]+1);
return 0;
}
运行结果:
0x7ffd0534fc40
0x7ffd0534fc40
0x7ffd0534fc40
0x7ffd0534fc44
0x7ffd0534fc68
0x7ffd0534fc44
buf+1 表示数组首地址+1元素大小,因为buf的类型是int*
&buf+1 表示数组首地址+1数组大小,因为&buf的类型是:int(*)[10]
在定义数组指针时,尤其要注意这一点!
#include
int main()
{
int buf[10] = {0,1,2,3,4,5,6,7,8,9};
int i = 0;
for(i = 0; i < sizeof(buf)/sizeof(buf[0]); i++)
{
printf("buf[%d] = %d\n",i,buf[i]);
}
return 0;
}
运行结果:
buf[0] = 0
buf[1] = 1
buf[2] = 2
buf[3] = 3
buf[4] = 4
buf[5] = 5
buf[6] = 6
buf[7] = 7
buf[8] = 8
buf[9] = 9
#include
int main()
{
int buf[10] = {0,1,2,3,4,5,6,7,8,9};
int i = 0;
for(i = 0; i < sizeof(buf)/sizeof(buf[0]); i++)
{
printf("buf[%d] = %d\n",i,*(buf+i));//重点:*buf是首地址常量,*(buf+1)表示第二个元素地址
}
return 0;
}
*buf是首地址常量,不能用*(buf++),因为++只对变量操作,不能对常量进行操作
可以用*(buf+1)表示第二个元素地址
#include
int main()
{
int buf[10] = {0,1,2,3,4,5,6,7,8,9};
int i = 0;
int *p = buf;//定义 指向数组的指针变量
// int *p;
// p = buf;也可以
for(i = 0; i < sizeof(buf)/sizeof(buf[0]); i++)
{
printf("buf[%d] = %d\n",i,*(p++));//变量可以++操作
}
return 0;
}
由于p是指针变量,因此可以p++
int *p = null;
int buf[10] = {0};
p = buf;//正确
p = &buf;//错误
因为buf 和 p的类型一致,都是int*;
而&buf的类型是: Int(*)[10];
#include
typedef unsigned char uint8_t; //1字节
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
int main(void)
{
uint8_t a = 10;
uint8_t b = 8;
/*定义一个函数指针*/
uint8_t (*func_ptr)(uint8_t, uint8_t);
/*将函数名赋值给函数指针*/
func_ptr = cal_sum;
printf("%d + %d = %p\r\n", a, b, func_ptr);//0x地址
printf("%d + %d = %d\r\n", a, b, func_ptr);//十进制地址
printf("%d + %d = %d\r\n", a, b, func_ptr(a,b));//函数返回值
}
运行结果:
10 + 8 = 0x401122
10 + 8 = 4198690
10 + 8 = 18
使用返回值时,函数指针要完整包含形参,仅有函数名时返回的仅仅是指针值(即函数地址)。
#include
typedef unsigned char uint8_t; //1字节
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
int main(void)
{
uint8_t a = 10;
uint8_t b = 8;
/*使用typedef定义一个函数指针*/
typedef uint8_t (*func_ptr)(uint8_t, uint8_t);
/*使用定义后的函数指针声明一个函数指针变量 pFun*/
func_ptr pFun;
/*将这个pFun指向了cal_sum函数*/
pFun = cal_sum;
printf("%d + %d = %p\r\n", a, b, pFun);//0x地址
printf("%d + %d = %d\r\n", a, b, pFun);//十进制地址
printf("%d + %d = %d\r\n", a, b, pFun(a,b));//函数返回值
printf("%d + %d = %p\r\n", a, b, cal_sum);//0x地址
printf("%d + %d = %d\r\n", a, b, cal_sum);//十进制地址
printf("%d + %d = %d\r\n", a, b, cal_sum(a,b));//函数返回值
}
运行结果:
10 + 8 = 0x401122
10 + 8 = 4198690
10 + 8 = 18
10 + 8 = 0x401122
10 + 8 = 4198690
10 + 8 = 18
使用typedef的优点在于:
先定义一种类型的函数指针
然后使用此函数指针声明多个同类型的函数指针变量
再给此函数指针变量 赋值 函数(要求同类型)
同时注意:函数指针使用时也要包含形参,不能仅用函数名或指针名,因为仅仅是一个指针
顾名思义,指向结构体的指针
# include
# include
struct AGE
{
int year;
int month;
int day;
};
struct STUDENT
{
char name[20]; //姓名
int num; //学号
struct AGE birthday; //生日
float score; //分数
};
int main(void)
{
struct STUDENT student1; /*用struct STUDENT结构体类型定义结构体变量student1*/
struct STUDENT *p = NULL; /*定义一个指向struct STUDENT结构体类型的指针变量p*/
p = &student1; /*p指向结构体变量student1的首地址, 即第一个成员的地址*/
strcpy((*p).name, "小明"); //(*p).name等价于student1.name
(*p).birthday.year = 1989;
(*p).birthday.month = 3;
(*p).birthday.day = 29;
(*p).num = 1207041;
(*p).score = 100;
printf("name : %s\n", (*p).name); //(*p).name不能写成p
printf("birthday : %d-%d-%d\n", (*p).birthday.year, (*p).birthday.month, (*p).birthday.day);
printf("num : %d\n", (*p).num);
printf("score : %.1f\n", (*p).score);
return 0;
}
运行结果:
name : 小明
birthday : 1989-3-29
num : 1207041
score : 100.0
我们看到,用指针引用结构体变量成员的方式是:
(*指针变量名).成员名
注意,*p 两边的括号不可省略,因为成员运算符“.”的优先级高于指针运算符“*”,所以如果 *p 两边的括号省略的话,那么 *p.num 就等价于 *(p.num) 了。
从该程序也可以看出:因为指针变量 p 指向的是结构体变量 student1 第一个成员的地址,即字符数组 name 的首地址,所以 p 和 (*p).name 是等价的。
但是,“等价”仅仅是说它们表示的是同一个内存单元的地址,但它们的类型是不同的。指针变量 p 是 struct STUDENT* 型的,而 (*p).name 是 char* 型的。所以在 strcpy 中不能将 (*p).name 改成 p。用 %s 进行输入或输出时,输入参数或输出参数也只能写成 (*p).name 而不能写成 p。
同样,虽然 &student1 和 student1.name 表示的是同一个内存单元的地址,但它们的类型是不同的。&student1 是 struct STUDENT* 型的,而 student1.name 是 char* 型的,所以在对 p 进行初始化时,“p=&student1;”不能写成“p=student1.name”。因为 p 是 struct STUDENT* 型的,所以不能将 char* 型的 student1.name 赋给 p。
另一种引用方式:
指针变量名->成员名
来代替,它们是等价的。“->”是“指向结构体成员运算符”,它的优先级同结构体成员运算符“.”一样高。p->num 的含义是:指针变量 p 所指向的结构体变量中的 num 成员。p->num 最终代表的就是 num 这个成员中的内容。
但是要注意的是,只有“指针变量名”后面才能加“->”,千万不要在成员名如 birthday 后面加“->”。
综上所述,以下 3 种形式是等价的:
其中第 3 种方式很重要,通常都是使用这种方式,另外两种方式用得不多。后面讲链表的时候用的也都是第 3 种方式。
很多时候我们一般在结构体中定义函数指针用的比较多一点。下面再举一个简单的例子。
#include
typedef unsigned char uint8_t;
/****************************************
* 函数指针结构体 开发者写的结构体
***************************************/
typedef struct
{
uint8_t (*p_sum)(uint8_t, uint8_t);
uint8_t (*p_sub)(uint8_t, uint8_t);
uint8_t (*p_mul)(uint8_t, uint8_t);
float (*p_div)(uint8_t, uint8_t);
} Operation_T;
/*声明结构体变量g_Operation*/
Operation_T g_Operation;
/*使用者写的回调函数*/
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
/*使用者写的回调函数*/
uint8_t cal_sub(uint8_t a, uint8_t b)
{
return a - b;
}
/*使用者写的回调函数*/
uint8_t cal_mul( uint8_t a, uint8_t b)
{
return a * b;
}
/*使用者写的回调函数*/
float cal_div(uint8_t a, uint8_t b)
{
return a / b;
}
/*结构体变量g_Operation初始化*/
Operation_T g_Operation = {cal_sum, cal_sub, cal_mul, cal_div};
int main(void)
{
uint8_t a = 10;
uint8_t b = 8;
/*使用函数指针调用函数*/
printf("%d\r\n", g_Operation.p_sum(a, b));
printf("%d\r\n", g_Operation.p_sub(a, b));
printf("%d\r\n", g_Operation.p_mul(a, b));
printf("%f\r\n", g_Operation.p_div(a, b));
}
运行结果:
18
2
80
1.000000
# include
struct STU
{
char name[20];
int age;
char sex;
char num[20];
};
int main(void)
{
struct STU stu[3] = {{"小红", 22, 'F', "Z1207031"}, {"小明", 21, 'M', "Z1207035"}, {"小七", 23, 'F', "Z1207022"}};
struct STU *p = stu;
for (; pname, p->age, p->sex, p->num);
}
return 0;
}
运行结果:
name:小红; age:22; sex:F; num:Z1207031
name:小明; age:21; sex:M; num:Z1207035
name:小七; age:23; sex:F; num:Z1207022
此时指针变量 p 就指向了结构体数组的第一个元素,即指向 stu[0]。我们知道,当一个指针指向一个数组后,指针就可以通过移动的方式指向数组的其他元素。
这个原则对结构体数组和结构体指针同样适用,
所以 p+1 就指向 stu[1] 的首地址;p+2 就指向 stu[2] 的首地址……所以只要利用 for 循环,指针就能一个个地指向结构体数组元素。
同样需要注意的是,要将一个结构体数组名赋给一个结构体指针变量,那么它们的结构体类型必须相同。
此外同前面“普通数组和指针的关系”一样,当指针变量 p 指向 stu[0] 时,p[0] 就等价于 stu[0];p[1] 就等价于 stu[1];p[2] 就等价于 stu[2]……所以 stu[0].num 就可以写成 p[0].num,其他同理。下面将上面的程序用 p[i] 的方式修改一下:
# include
struct STU
{
char name[20];
int age;
char sex;
char num[20];
};
int main(void)
{
struct STU stu[3] = {{"小红", 22, 'F', "Z1207031"}, {"小明", 21, 'M', "Z1207035"}, {"小七", 23, 'F', "Z1207022"}};
struct STU *p = stu;
int i = 0;
for (; i<3; ++i)
{
printf("name:%s; age:%d; sex:%c; num:%s\n", p[i].name, p[i].age, p[i].sex, p[i].num);
}
return 0;
}
待补充
回调函数就是一个被作为参数传递的函数;
回调函数是一个通过函数指针方式,被调用的函数;
函数指针作为函数的形参我们把这个函数指针称为“回调函数”
1.回调函数是一个函数指针
2.回调函数是一个作为其他函数形参的函数指针
3.回调函数即包含返回值,也包含形参;也可能是多个函数指针的嵌套
4.把一段可执行的代码像参数传递那样传给其他代码,如果这段代码被立即执行称为“同步回调”,如果之后再执行称为“异步回调”。
5.回调函数不是由实现方调用,而是在特定条件下,由另一方调用。
6.例如,同一功能函数,通过函数指针调用不同的回调函数,达到实现不同的功能目的。
/*具有返回值和两个形参的一个函数*/
uint8_t compute_func1(uint8_t, uint8_t);
/*常规定义一个函数指针*/
uint8_t (*func_ptr)(uint8_t, uint8_t);
/*将函数名赋值给函数指针,两者必须同类型*/
func_ptr = compute_func1
/*包含回调函数“uint8_t (*func_ptr)(uint8_t, uint8_t)”作为形参的一个功能函数compute_func2 */
uint8_t compute_func2(uint8_t (*func_ptr)(uint8_t, uint8_t),uint8_t, uint8_t);
在这个函数当中,通过该函数指针调用的函数被称为回调函数
回调函数:uint8_t compute_func(uint8_t, uint8_t)
功能函数:uint8_t compute_func2(uint8_t, uint8_t)
这种开发方式的用途非常广泛。具体来说,在回调函数的应用场景当中,会出现两个角色。分别是某功能函数的开发者以及该功能函数的使用者。compute_func函数就是开发者写的函数,是非常牛逼的写库和底层的那一类人写的函数,我们每一个单片机的使用者,需要写出各种各样的具体的功能函数,只要我们写得功能函数的形参和返回值和函数指针的类型相同就可以了。
#include
typedef unsigned char uint8_t; //1字节
/*使用者写的函数*/
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
/*开发者写的函数*/
uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b)
{
return func_ptr(a, b);
}
int main(void)
{
uint8_t a = 10;
uint8_t b = 8;
printf("compute_func(cal_sum,a,b) =%d\r\n", compute_func(cal_sum, a, b));
printf("compute_func地址 =%p\r\n", compute_func);
}
运行结果:
compute_func(cal_sum,a,b) =18
compute_func地址 =0x40113c
注意:这里要注意的是我们使用者写的函数的类型一定要于开发者写的回调函数类型一样,比如形参和返回值的类型要一样。不然肯定不能调用的。
换句话说就是,下面的这两个函数的形参和返回值都必须是相同的类型才可以,不能一个有返回值一个没有,明明函数指针有两个形参,你写的函数却只有一个形参也是不行的。
//正确写法:
uint8_t cal_mul(uint8_t , uint8_t )
uint8_t (*func_ptr)(uint8_t, uint8_t)
//错误写法:
void cal_mul(uint8_t , uint8_t ) //你写的函数却没有返回值
uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有返回值
//错误写法:
uint8_t cal_mul(uint8_t) //你写的函数却只有一个形参
uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有两个形参
#include
int InputData[100]={0};
int OutputData[100]={0};
/*定义回调函数*/
void CallBack_FFT_Function(int *inputData,int *outputData,int num)
{
while(num--)
{
printf("国内疫情结束,再也不用做核酸了!\r\n");
}
}
/*用来注册回调函数的功能函数*/
void TaskA(void (*fft)(int*,int*,int))
{
fft(InputData,OutputData,5);
}
int main(void)
{
/*注册FFT_Function作为回调*/
TaskA(CallBack_FFT_Function);
return 0;
}
上面的代码中CallBack_FFT_Function就
是回调函数,TaskA
是用来注册回调函数的功能函数。可以看到用来注册回调函数的功能函数中申明的函数指针必须和回调函数的类型完全相同。
在我们的代码中具有回调功能所需的元素是:
将被调用的回调函数
cal_sum
将用于访问回调函数的函数指针
p_sum
将调用回调函数的调用函数
compute_func
在stm32的HAL库中,是使用了大量的回调函数的,串口、定时器等外设都是有对应的回调函数的,回调机制可以更好地分离代码,应用层和驱动层完全分离,降低耦合性。
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//串口发送完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//串口接收完成回调
串口中断发送/接收完成之后,会进入该函数,该函数为空函数,用户需自行修改。