news 2026/4/18 13:45:33

深入理解C语言指针传参:为什么这个ADC读取函数必须用指针?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解C语言指针传参:为什么这个ADC读取函数必须用指针?

深入理解C语言指针传参:为什么这个ADC读取函数必须用指针?

一、一个经典困惑:参数传递的两面性

在嵌入式开发中,你是否曾困惑过这样的问题:为什么有些函数调用时可以直接传变量,有些却必须用指针?今天我们就通过一个实际的ADC读取函数来彻底解开这个谜团。

先看你的函数原型:

staticHAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef*hadc,volatilefloat*adc_value);

与直接传值的版本对比:

// 这个版本为什么不工作?staticHAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef hadc,volatilefloatadc_value);

二、值传递 vs 指针传递:内存视角看本质

2.1 参数传递的底层机制

在C语言中,所有函数参数传递都是按值传递。但这不意味着指针很特殊,而是我们通过传递"地址值"来实现间接访问。

// 示例:理解内存分配voidfunction_value(intx){// x是局部变量,有独立内存空间x=100;// 只修改了副本}voidfunction_pointer(int*x){// x是局部指针变量,存放地址*x=100;// 通过地址修改原始数据}intmain(){intoriginal=5;// 值传递:传递5这个值function_value(original);// original还是5// 指针传递:传递&original这个地址值function_pointer(&original);// original变为100}

内存布局对比

值传递: ┌─────────────┐ ┌─────────────┐ │ main栈帧 │ │ 函数栈帧 │ │ original=5 │───→│ x=5 │ └─────────────┘ └─────────────┘ 修改x不影响original 指针传递: ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ main栈帧 │ │ 函数栈帧 │ │ 数据区域 │ │ original=5 │ │ x=&original │───→│ [地址] │ │ [地址] │←───┴─────────────┘ │ 值=100 │ └─────────────┘ └─────────────┘

2.2 你的ADC函数为什么必须用指针?

函数中:

*adc_value=(float)HAL_ADC_GetValue(hadc);

这里需要完成的任务是:将ADC读取的结果存储到调用者提供的变量中

如果使用值传递:

// 错误版本:值传递staticHAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef hadc,volatilefloatadc_value){// ... 读取ADCadc_value=(float)HAL_ADC_GetValue(&hadc);// 只修改了局部副本!returnstatus;}// 调用者floatsensor_value;Read_ADC_Channel(my_adc,sensor_value);// sensor_value不会被更新!

调用者永远获取不到ADC的读取结果,因为函数修改的只是adc_value本地副本

三、什么时候用指针,什么时候用普通变量?

3.1 指针参数的使用场景(必须用)

场景示例为什么必须用指针
修改外部变量void set_value(int* x, int val)需要改变调用者的变量值
输出参数bool read_sensor(float* output)函数需要"返回"多个值
传递大结构体void process_data(LargeStruct* data)避免复制整个结构体的开销
数组参数void sort_array(int arr[], int size)数组名退化为指针
动态内存管理void allocate_buffer(char** buf)需要修改指针本身的值
硬件寄存器访问void write_register(volatile uint32_t* reg)直接操作内存映射地址

3.2 普通变量的使用场景(可以直接用)

场景示例为什么可以用普通变量
只读输入参数float calculate_area(float radius)只需要值,不需要修改
简单类型参数int add(int a, int b)复制开销小,代码清晰
临时计算结果void process_temp(int temp)不需要影响外部
枚举/常量参数void set_mode(OperationMode mode)传递的是值,不是状态

3.3 你的ADC函数属于哪一类?

让我们分析你的函数需求:

  1. 需要返回ADC转换值→ 输出参数 → 需要指针
  2. ADC值需要被外部使用→ 修改外部变量 → 需要指针
  3. 可能有多个调用者需要结果→ 共享存储 → 需要指针

因此,指针是必然选择。

四、深入分析:volatile关键字的作用

在你的函数中,参数被声明为volatile float* adc_value,这增加了一层复杂性。为什么需要volatile

// 带volatile的指针声明volatilefloat*adc_value;// 指向volatile float的指针// 对比普通指针float*normal_ptr;// 指向float的指针

volatile的作用

  • 告诉编译器这个指针指向的数据可能被硬件异步修改
  • 防止编译器优化对该地址的读写操作
  • 每次访问都从内存重新读取,不使用缓存值

在你的应用中,可能是:

  1. ADC数据寄存器映射到特定内存地址
  2. 中断服务程序可能修改这个值
  3. 多任务环境中被其他任务修改

五、替代方案分析:不用指针行不行?

方案1:通过返回值传递结果(不适用)

// 尝试1:只返回ADC值,无法返回状态floatRead_ADC_Channel(ADC_HandleTypeDef*hadc){// 如果出错怎么办?无法返回错误状态}// 尝试2:返回结构体(可行但不如指针高效)typedefstruct{HAL_StatusTypeDef status;floatvalue;}ADC_Result;ADC_ResultRead_ADC_Channel(ADC_HandleTypeDef*hadc){ADC_Result result;// ... 读取逻辑result.value=(float)HAL_ADC_GetValue(hadc);result.status=status;returnresult;// 结构体复制开销}

缺点:结构体返回涉及内存复制,对于频繁调用的函数效率较低。

方案2:使用全局变量(不推荐)

// 全局变量方式volatilefloatg_adc_result;staticHAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef*hadc){// ... 读取逻辑g_adc_result=(float)HAL_ADC_GetValue(hadc);returnstatus;}// 调用者floatmy_value=g_adc_result;// 需要额外步骤获取值

缺点

  1. 破坏了函数封装性
  2. 多个ADC通道需要多个全局变量
  3. 线程不安全,容易产生竞争条件

方案3:当前设计(最优选择)

// 当前设计:通过指针参数返回结果staticHAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef*hadc,volatilefloat*adc_value){// ... 读取逻辑*adc_value=(float)HAL_ADC_GetValue(hadc);returnstatus;}// 调用清晰,一个函数调用完成所有操作floatsensor_value;HAL_StatusTypeDef status=Read_ADC_Channel(&my_adc,&sensor_value);

优点

  1. 函数职责单一且完整
  2. 调用接口清晰
  3. 无额外内存复制
  4. 支持多通道重用

六、实战对比:三种调用方式的性能分析

测试代码:

#include<stdint.h>#include<time.h>#defineADC_SIMULATED_VALUE2048.0f#defineITERATIONS1000000// 方式1:指针参数(你的方式)typedefenum{ADC_OK=0,ADC_ERROR}ADC_Status;ADC_Statusread_adc_ptr(float*result){*result=ADC_SIMULATED_VALUE;returnADC_OK;}// 方式2:返回结构体typedefstruct{ADC_Status status;floatvalue;}ADC_Result;ADC_Resultread_adc_struct(void){ADC_Result res;res.value=ADC_SIMULATED_VALUE;res.status=ADC_OK;returnres;}// 方式3:全局变量floatg_adc_global;ADC_Statusread_adc_global(void){g_adc_global=ADC_SIMULATED_VALUE;returnADC_OK;}voidbenchmark(void){clock_tstart,end;floatvalue;ADC_Result result;ADC_Status status;// 测试指针方式start=clock();for(inti=0;i<ITERATIONS;i++){status=read_adc_ptr(&value);}end=clock();printf("指针方式: %.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);// 测试结构体方式start=clock();for(inti=0;i<ITERATIONS;i++){result=read_adc_struct();}end=clock();printf("结构体方式: %.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);// 测试全局变量方式start=clock();for(inti=0;i<ITERATIONS;i++){status=read_adc_global();value=g_adc_global;}end=clock();printf("全局变量方式: %.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);}

典型结果(STM32F4 @ 168MHz):

  • 指针方式:最快,直接内存操作
  • 结构体方式:慢30-50%,涉及结构体复制
  • 全局变量方式:与指针相当,但代码结构差

七、高级技巧:多返回值函数的指针使用

你的函数只返回一个值,但有时需要返回多个值:

// 示例:需要返回ADC值和状态标志typedefstruct{floatvalue;uint8_toverrange;uint8_tinvalid;}ADC_DetailedResult;staticHAL_StatusTypeDefRead_ADC_Detailed(ADC_HandleTypeDef*hadc,ADC_DetailedResult*result){uint32_traw=HAL_ADC_GetValue(hadc);// 设置多个输出值result->value=(float)raw;result->overrange=(raw>0xFFF0)?1:0;result->invalid=(raw==0xFFFF)?1:0;return(result->invalid)?HAL_ERROR:HAL_OK;}// 调用ADC_DetailedResult adc_info;status=Read_ADC_Detailed(&my_adc,&adc_info);printf("值: %.2f, 过载: %d, 无效: %d\n",adc_info.value,adc_info.overrange,adc_info.invalid);

八、常见错误与调试技巧

错误1:忘记取地址符

// 错误floatvalue;status=Read_ADC_Channel(adc,value);// 缺少&// 正确status=Read_ADC_Channel(adc,&value);

错误2:指针未初始化

// 错误float*uninitialized_ptr;*uninitialized_ptr=10.0f;// 段错误!// 正确floatvalue;float*ptr=&value;*ptr=10.0f;

错误3:误解const指针

// 这个指针指向的数据是const,不能通过指针修改voidread_only(constfloat*data){// *data = 10.0f; // 编译错误!}// 这个指针本身是const,不能指向其他地址voidfixed_pointer(float*constdata){// data = &other; // 编译错误!*data=10.0f;// 可以}// 双重const:都不能改voidfully_const(constfloat*constdata){// data = &other; // 错误// *data = 10.0f; // 错误}

调试技巧:使用assert验证指针

#include<assert.h>staticHAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef*hadc,volatilefloat*adc_value){// 参数检查assert(hadc!=NULL);assert(adc_value!=NULL);// ... 函数逻辑}

九、设计原则总结

9.1 何时使用指针参数的决策流程

开始 │ ├─ 函数是否需要修改参数值? │ ├─ 是 → 使用指针 │ └─ 否 → 继续 │ ├─ 参数是否是大型结构体/数组? │ ├─ 是 → 使用指针(提高效率) │ └─ 否 → 继续 │ ├─ 是否需要返回多个值? │ ├─ 是 → 使用指针参数 │ └─ 否 → 继续 │ └─ 使用普通变量参数

9.2 你的ADC函数设计评价

优点

  1. 职责单一:读取ADC并返回结果和状态
  2. 接口清晰:调用者明确知道哪些参数会被修改
  3. 效率高:避免不必要的内存复制
  4. 可重用:适用于任何ADC通道和存储变量

改进建议

  1. 添加参数有效性检查
  2. 考虑添加超时机制
  3. 如果可能,支持DMA方式读取

9.3 最佳实践口诀

“修外部,用指针;仅读取,值传递”

“大结构,指针优;多返回,指针凑”

“寄存器,volatile;并发访,要小心”

十、拓展思考:C++中的引用参数

如果你是C++开发者,还可以使用引用:

// C++方式:引用参数HAL_StatusTypeDefRead_ADC_Channel(ADC_HandleTypeDef*hadc,volatilefloat&adc_value)// 注意&符号{adc_value=(float)HAL_ADC_GetValue(hadc);returnstatus;}// 调用(更简洁)floatvalue;status=Read_ADC_Channel(&my_adc,value);// 不需要&

引用提供了指针的便利性,同时语法更简洁安全,但这是C++特性,在C中不可用。


总结:在你的ADC读取函数中,使用指针参数不是偶然选择,而是必然要求。它体现了以下几个设计考量:

  1. 功能需求:需要修改调用者的变量
  2. 效率考量:避免不必要的数据复制
  3. 接口设计:清晰表达函数的输入输出
  4. 资源管理:让调用者控制存储位置

记住这个黄金法则:如果一个函数需要"返回"结果到调用者提供的存储位置,那么指针就是唯一正确的选择。这不是语言的限制,而是问题本质决定的——数据的归属权在调用者,函数只是使用者

理解这一点,你就能在未来的设计中自信地选择正确的参数传递方式,写出既高效又清晰的代码。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 12:32:53

揭秘Open-AutoGLM在macOS上的运行机制:5步实现本地大模型自动化推理

第一章&#xff1a;揭秘Open-AutoGLM在macOS上的运行机制 Open-AutoGLM 是一款基于 AutoGPT 架构设计的开源语言模型框架&#xff0c;专为本地化推理与自动化任务执行优化。在 macOS 平台上&#xff0c;其运行依赖于 Metal 加速技术与 Python 环境的深度集成&#xff0c;使得大…

作者头像 李华
网站建设 2026/4/18 10:07:39

PaddlePaddle博物馆智能导览系统

PaddlePaddle博物馆智能导览系统 在一座安静的博物馆展厅里&#xff0c;一位游客举起手机对准一幅古画展板。不到两秒&#xff0c;耳边便响起清晰的声音&#xff1a;“这幅《千里江山图》由北宋王希孟创作&#xff0c;采用青绿山水技法……”与此同时&#xff0c;屏幕上浮现出详…

作者头像 李华
网站建设 2026/4/18 3:41:46

第101章 PNP的曙光(悦儿)

弦光研究院深处,有一间特殊的静室。这里没有窗户,四壁和天花板皆由吸音材料覆盖,地面是冰冷的黑色金属板。唯一的光源来自房间中央悬浮着的一个全息投影装置,此刻正投射出一个极其复杂、不断缓慢变幻着的多维几何结构。这便是悦儿的“思维熔炉”,一个将她内心对数学宇宙的…

作者头像 李华
网站建设 2026/4/18 2:59:49

基于SpringBoot+Vue的智能AI问答个人健康管理系统_个人健康饮食搭配推荐系统 rb45796s 开题报告任务书

目录已开发项目效果实现截图开发技术介绍核心代码参考示例1.建立用户稀疏矩阵&#xff0c;用于用户相似度计算【相似度矩阵】2.计算目标用户与其他用户的相似度系统测试总结源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;已开发项目效果…

作者头像 李华
网站建设 2026/4/18 2:14:03

【Open-AutoGLM没反应】:3大原因深度剖析与5步快速恢复指南

第一章&#xff1a;Open-AutoGLM没反应当使用 Open-AutoGLM 时&#xff0c;若模型调用后无任何响应或输出停滞&#xff0c;通常由环境配置、依赖冲突或 API 调用逻辑错误导致。排查此类问题需从服务启动状态、请求格式和运行时日志入手。检查服务是否正常启动 确保 Open-AutoGL…

作者头像 李华
网站建设 2026/4/18 8:41:55

【Open-AutoGLM报错全解析】:20年专家亲授5大常见错误排查方案

第一章&#xff1a;Open-AutoGLM报错全解析概述在使用 Open-AutoGLM 框架进行自动化大语言模型调用与任务编排时&#xff0c;开发者常会遇到各类运行时错误与配置异常。这些报错可能源于环境依赖缺失、API密钥未正确配置、输入格式不符合规范&#xff0c;或模型服务端响应异常等…

作者头像 李华