C指针传参的一些思考

一、仅操作变量名的子函数,在主函数中无法真正实现值传递

如下:

void swap_1(int num1, int num2) 作为主函数的子函数,被main函数调用;

在子函数内部实现了num1 和 num2的交换(子函数中加打印可看出),但swap_1被调用后就直接释放掉了,其栈空间内存储的temp,num1,num2也同时被释放调了;因此对于main主函数中的变量a和b,并没有起到赋值的作用。

值传递是单向的,无法通过形参(num1,num2)实现对实参(a,b)的修改。

#include <stdio.h>
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 <stdio.h>

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 <stdio.h>
#include <stdlib.h>
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修饰变量的值的修改

一般来讲,const a 表示变量a的值不能被修改;

但是变量a被指针p引用后,通过修改p所指向的空间内容,实现了对const 变量a的值的修改

注:不同编译器可能会有差异 !

#include <stdio.h>
int main( )
{
    int const a = 3;
    int *p = (int*)&a ;
	*p = 5;
    printf("a = %d,\n", a);
 
    return 0;
}

/*
 * result:
 * a = 5
 */

五、数组&传参

1、数组的地址

int buf[10] = {0} ;

buf:1)表示数组名,如sizeof(buf);2)表示数组元素首地址(常量),即&buf[0],类型是int*

buf[0]:表示数组第一个元素,既可以作为右值被读取,也可以作为左值被写入;

&buf[0]:等价于buf的形式二,是一个地址常量(首元素地址、数组首地址),只能作为右值;

&buf:表示是数组首地址,是一个地址常量,只能作为右值;类型是int(*)[10]

#include <stdio.h>

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 <stdio.h>

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]

在定义数组指针时,尤其要注意这一点!

2、访问数组的几种方式

1)使用数组下标访问


#include <stdio.h>

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


2)使用指针常量访问 *(buf+i)

#include <stdio.h>

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)表示第二个元素地址

3)使用指针变量访问 *p = buf

#include <stdio.h>

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++

4)指针类型与数组类型的匹配

int *p = null;
int buf[10] = {0};

p = buf;//正确
p = &buf;//错误

因为buf 和 p的类型一致,都是int*;

而&buf的类型是: Int(*)[10];

六、函数指针传参

1、函数指针的定义

1)一般定义

#include <stdio.h>
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

使用返回值时,函数指针要完整包含形参,仅有函数名时返回的仅仅是指针值(即函数地址)。

2)使用typedef定义后声明,再赋值

#include <stdio.h>
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的优点在于:

先定义一种类型的函数指针

然后使用此函数指针声明多个同类型的函数指针变量

再给此函数指针变量 赋值 函数(要求同类型)

同时注意:函数指针使用时也要包含形参,不能仅用函数名或指针名,因为仅仅是一个指针

七、结构体传参

1、结构体指针

顾名思义,指向结构体的指针

# include <stdio.h>
# include <string.h>
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 种方式。

2、函数指针结构体

很多时候我们一般在结构体中定义函数指针用的比较多一点。下面再举一个简单的例子。

#include <stdio.h>
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

3、指向结构体数组的指针

# include <stdio.h>
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 (; p<stu+3; ++p)
    {
        printf("name:%s; age:%d; sex:%c; num:%s\n", p->name, 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 <stdio.h>
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;
}

4、指向“函数指针结构体”的指针

待补充

八、回调函数

1、定义

回调函数就是一个被作为参数传递的函数;

回调函数是一个通过函数指针方式,被调用的函数;

函数指针作为函数的形参我们把这个函数指针称为“回调函数”

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 <stdio.h>
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)//函数指针有两个形参

2、回调函数的注册和使用

#include <stdio.h>

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是用来注册回调函数的功能函数。可以看到用来注册回调函数的功能函数中申明的函数指针必须和回调函数的类型完全相同

3、回调函数在嵌入式中的具体实例

在我们的代码中具有回调功能所需的元素是:

  • 将被调用的回调函数cal_sum

  • 将用于访问回调函数的函数指针p_sum

  • 将调用回调函数的调用函数compute_func

在stm32的HAL库中,是使用了大量的回调函数的,串口、定时器等外设都是有对应的回调函数的,回调机制可以更好地分离代码,应用层和驱动层完全分离,降低耦合性。

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);//串口发送完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//串口接收完成回调

串口中断发送/接收完成之后,会进入该函数,该函数为空函数,用户需自行修改。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值