一、概述
C语言基础大部分都很好理解,唯一入手门槛比较高的,就数指针了。指针是C语言的一大特色,因为指针,C语言可以极度灵活,但也因为指针,C语言变得很不安全。指针就是一把双刃剑,用好了可以让你如虎添翼,用得不好,也会让你找问题的时候摸不着头脑。
二、引言
大多数时候,其实不用指针也可以实现很多功能,而且其实最初我们学的指针,大部分应该就是下面这种,通过指针将函数内的计算结果返回给上层。
/* 实现传入p值后,把p+data的结果返回给p */
void Add(uint8_t *p, uint8_t data)
{
(*p) += data;
}
int main(void)
{
uint8_t a = 1;
Add(&a, 1);
/* 打印结果为a = 2 */
printf("a = %d\n", a);
}
当然上面这种就是指针很常见的一种用法,有人可能会说了,要返回函数内的值,直接return结果不也可以实现么,就像下面这样。
/* 实现传入p值后,把p+data的结果通过函数返回值返回上层 */
uint8_t Add(uint8_t p, uint8_t data)
{
return (p + data);
}
int main(void)
{
uint8_t a = 1;
a = Add(a, 1);
/* 打印结果为a = 2 */
printf("a = %d\n", a);
}
确实这样子也可以实现,但这里我们不讨论有哪些可以替代指针的,而是要从原理的角度来分析,指针可以做出哪些不可替代的事情。
三、原理
指针的本质也是个变量,只不过通过*可以把当前变量的值作为地址来进行使用,而通过&则可以获取当前变量的地址。然而就是这么简单的两个操作,可以玩出各种花样来。所以指针难的不是它的操作方式,而是因为想要搞懂指针,得从内存结构学起。
首先是变量,已知变量在内存里是存在一个空间的,比如一个16位的变量,在内存空间里就占用两个字节。那怎么区分内存里的不同变量呢,这里就引入内存地址这个概念,不同的内存地址表示不同的变量,并且一个内存地址表示一个字节的变量空间。所以当使用一个16位的变量时,实际在内存里是占用了两个地址。
了解了变量与内存的关系后,接下来就是指针了,当我们对变量A使用取址符&进行取址时,其实是获取了变量A在内存中的地址。那这个地址是个什么值呢?这就跟系统架构有关系了,如果是32位的系统,则其地址也是一个32位的变量,64位系统则是64位的变量。那既然是变量,那也需要有个内存用来存放其数值,所以在32位系统中,定义一个指针,其指针本身就是一个占用4个字节空间的变量。
既然都是变量,那指针变量和普通变量有什么关系和区别呢?
类别 | 普通变量 | 指针变量 |
---|---|---|
占用空间 | 根据数据类型来定 | 根据寻址方式来定 |
数值含义 | 数值 | 内存地址 |
四、应用场景
指针有两种使用方式,一种是获取其值,即我们常用的指针引用,即变量指针,另一种则是跳转,即函数指针。接下来我们就要利用"&“和”*"这两个符号,把指针玩出些花儿。
- 变量指针
变量指针,即指向变量的指针,通过对指针的引用可索引到指针指向的变量内存。下面是一个最简单的指针引用的例子。
#include <stdint.h>
int main(void)
{
uint8_t *a = 0x00002000;
/* 对地址为0x00002000的变量进行赋1的操作 */
*a = 1;
}
初学者可能对上面的例子不大感冒,下面换个教材里的写法。其实把下面&a的值看成0x00002000,那就跟上面的例子是一样的作用了。
#include <stdint.h>
#include <stdio.h>
int main(void)
{
uint8_t a = 0;
uint8_t *b = &a;
/* 打印出a的值为0 */
printf("%d", a);
*b = 1;
/* 通过指针b修改a的值后,打印a的值为1 */
printf("%d", a);
}
根据对指针地址偏移进行引用,可快速对批量内存进行操作。一般指针的偏移是根据指针引用后的数据类型来进行偏移的,怎么理解呢?看下以下例子。
#include <stdint.h>
#include <stdio.h>
struct tagSomeType
{
uint16_t A;
uint16_t B;
uint16_t C;
……
};
int main()
{
uint8_t *pU8;
uint16_t *pU16;
struct tagSomeType *pSomeType;
printf("%d", ((uint32_t)(pU8 + 1)) - ((uint32_t)(pU8)));
printf("%d", ((uint32_t)(pU16 + 1)) - ((uint32_t)(pU16)));
printf("%d", ((uint32_t)(pSomeType + 1)) - ((uint32_t)(pSomeType)));
}
在32位系统下运行如上代码,结果是1,2,sizeof(struct tagSomeType)。这说明了什么,即当前指针的偏移是根据其引用后的数据类型来决定的,当指针的引用类型为uint8_t时,指针地址加1只是增加一个字节的偏移,但如果指针引用类型的数据为一个结构体,则指针地址加1偏移的是一个结构体的大小。
有时候可以把数组当成结构体去操作,这样做的好处是,当对用户提供封装成.lib文件的模块库时,用户不会得知模块内部的数据结构是怎么样的。这种用法在RTX操作系统中就有体现。
/***********************某模块的.c文件**************************/
/* 模块内部数据结构 */
struct tagModuleCB
{
uint32_t ParaA;
uint32_t ParaB;
};
/* 模块操作函数 */
void ModuleFunc(uint32_t *cb)
{
struct tagModuleCB *md_cb = (struct tagModuleCB *)cb;
md_cb->ParaA = 0;
md_cb->ParaB = 1;
}
/**************************************************************/
/***********************某模块的.h文件**************************/
/* 模块内部结构块大小 */
#define MODULE_SIZE (8UL)
/* 用于定义模块实体的辅助宏 */
#define MODULE_DECLARE(cb) uint32_t cb[(MODULE_SIZE + 3) / 4]
/* 对外操作接口 */
void ModuleFunc(uint32_t *cb);
/**************************************************************/
除了封装的作用外,还有一个作用,就是动态扩展。当然,这里的动态不是真的代码运行过程中动态变化,而是在修改代码后编译时动态扩展对应的内存空间。比如现在有个串口驱动模块结构体代码如下,当单片机存在2个串口时,要求一个串口要有100字节的缓存,另一个则要求至少要256字节缓存。那么根据下面模块代码的写法,定义两个实体时,都必须按最大的256字节去定义,这样第一个串口只用100字节,白白浪费156字节空间。
/* 定义串口模块结构块 */
struct tagUartCB
{
uint8_t Buff[];
uint32_t Size;
};
/* 底层获取串口接收到的数据的接口 */
extern uint8_t Get_UartData(uint8_t uart_id);
/* 接收中断处理函数 */
void Uart_RxIRQ(uint32_t *cb, uint8_t uart_id)
{
struct tagUartCB *uart_cb = (struct tagUartCB *)cb;
if (uart_cb)
{
/* 把Buff当成数组缓存的起始位置作偏移进行存储 */
uart_cb->Buff[uart_cb->Size++] = Get_UartData(uart_id);
}
}
/* 获取接收的数据 */
uint8_t *Uart_GetBuff(uint32_t *cb, uint32_t *size)
{
struct tagUartCB *uart_cb = (struct tagUartCB *)cb;
*size = uart_cb->Size;
/* 获取完数据后要把接收数据长度清0 */
uart_cb->Size = 0;
return (uart_cb->Buff);
}
而使用Declare的方式,可以完美解决上述的问题。
/***********************某模块的.c文件**************************/
/* 模块实体的空间分布情况
* +--------+
* | Buff |----+
* +--------+ |
* | Size | |
* +--------+ |
* | 预留 |<---+
* | 缓存 |
* +--------+
*/
/* 定义串口模块结构块 */
struct tagUartCB
{
uint8_t *Buff;
uint32_t Size;
};
/* 模块初始化 */
void Uart_Init(uint32_t *cb)
{
struct tagUartCB *uart_cb = (struct tagUartCB *)cb;
if (uart_cb)
{
/* 初始化时需要把当前缓存指针指向结构体末尾 */
uart_cb->Buff = uart_cb + 1;
}
}
/* 其他操作同上,这里就不再赘述 */
/**************************************************************/
/***********************某模块的.h文件**************************/
/* 模块内部结构块大小 */
#define UART_SIZE (8UL)
/* 用于定义模块实体的辅助宏 */
#define UART_DECLARE(cb, size) uint32_t cb[(UART_SIZE + size + 3) / 4]
/**************************************************************/
/***********************应用的.c文件***************************/
/* 定义串口1的实体大小 */
UART_DECLARE(Com1, 100);
/* 定义串口2的实体大小 */
UART_DECLARE(Com2, 256);
/**************************************************************/
当你把指针玩得更溜时,上面的代码还可以再改造一下,又可以省出4个字节的空间。此时是手上无指针,但处处是指针。(下面这种写法只适用于只有一段可变缓存,如果同个模块有多个可变缓存,则老老实实使用指针吧)
/***********************某模块的.c文件**************************/
/* 模块实体的空间分布情况
* +--------+
* | Size |
* +--------+
* | Buff |
* +--------+
* | 预留 |
* | 缓存 |
* +--------+
*/
/* 定义串口模块结构块 */
struct tagUartCB
{
uint32_t Size;
/* 这里也可以写成Buff[0],用数组来操作更方便,但不是所有的编译器都支持 */
uint8_t Buff;
};
/* 底层获取串口接收到的数据的接口 */
extern uint8_t Get_UartData(uint8_t uart_id);
/* 接收中断处理函数 */
void Uart_RxIRQ(uint32_t *cb, uint8_t uart_id)
{
struct tagUartCB *uart_cb = (struct tagUartCB *)cb;
if (uart_cb)
{
/* 把Buff当成数组缓存的起始位置作偏移进行存储 */
*(&(uart_cb->Buff) + uart_cb->Size++) = Get_UartData(uart_id);
}
}
/* 获取接收的数据 */
uint8_t *Uart_GetBuff(uint32_t *cb, uint32_t *size)
{
struct tagUartCB *uart_cb = (struct tagUartCB *)cb;
*size = uart_cb->Size;
/* 获取完数据后要把接收数据长度清0 */
uart_cb->Size = 0;
return (&uart_cb->Buff);
}
/**************************************************************/
/***********************某模块的.h文件**************************/
/* 模块内部结构块大小 */
#define UART_SIZE (4UL)
/* 用于定义模块实体的辅助宏 */
#define UART_DECLARE(cb, size) uint32_t cb[(UART_SIZE + size + 3) / 4]
/**************************************************************/
/***********************应用的.c文件***************************/
/* 定义串口1的实体大小 */
UART_DECLARE(Com1, 100);
/* 定义串口2的实体大小 */
UART_DECLARE(Com2, 256);
/**************************************************************/
- 函数指针
函数指针,就是指向函数的指针,好用得飞起,哪里会用到呢?比如现在要做一个boot,那么怎么从boot跳转到app呢?函数指针就是一个很好的选择。
/* 跳转函数 */
void JumpTo(uint32_t addr)
{
typedef void (*pFunc)(void);
/* 把传入的地址强转成函数指针,并调用 */
((pFunc)addr)();
}
int main(void)
{
/* 跳转至0x08001000地址运行 */
JumpTo(0x08001000);
return 0;
}
另外一个,也是最常使用的一种用法——钩子函数,也叫作回调函数。一般在分层架构的程序中有一个规则,那就是只能上层调用下层接口。但在嵌入式里,像中断函数这种,理应属于最下层,即硬件层,怎么去使用上层的接口呢?这时候回调函数就派上用场了。
举个串口的例子,比如现在接收到一个字节的数据,要把这个数据传递给上层,应用分层的思想,串口中断中不直接引用上层接口,而是通过一个回调函数来实现。具体实现如下。
/*********************串口底层模块.c**************************/
static void (*UsartRXCallback)(void);
void USART1_CallbackInit(void (*rx_cbk)(void))
{
UsartRXCallback = rx_cbk;
}
/* 串口的中断服务函数 */
void USART1_IRQHandler(void)
{
/* 如果触发接收中断标志,则执行接收回调 */
if (LL_USART_IsActiveFlag_RXNE(USART1))
{
/* 防止回调函数未初始化 */
if (UsartRXCallback)
{
UsartRXCallback();
}
}
}
/*************************************************************/
/*************************应用层.c*****************************/
void UART_DoSomething(void)
{
/* 需要在接收中断里执行的操作 */
}
int main(void)
{
/* 挂接回调函数 */
USART1_CallbackInit(UART_DoSomething);
}
/*************************************************************/
这样上层只需要调用USART1_CallbackInit接口,把需要在接收到数据时处理的函数传入其中,后续只需要等待触发串口中断即可。
- 函数指针、数组、结构体大融合
函数指针、结构体和数组融合后会出现什么化学反应呢?对,融合后就会变成我们的老朋友——表驱动。具体它的好处,就移步到另一篇文章——表驱动模式,里面会有详细说明。这里截取实际项目(FreeModbus)中使用的部分代码。
#define MB_FUNC_NONE ( 0 )
#define MB_FUNC_READ_COILS ( 1 )
#define MB_FUNC_READ_DISCRETE_INPUTS ( 2 )
#define MB_FUNC_WRITE_SINGLE_COIL ( 5 )
#define MB_FUNC_WRITE_MULTIPLE_COILS ( 15 )
#define MB_FUNC_READ_HOLDING_REGISTER ( 3 )
#define MB_FUNC_READ_INPUT_REGISTER ( 4 )
#define MB_FUNC_WRITE_REGISTER ( 6 )
#define MB_FUNC_WRITE_MULTIPLE_REGISTERS ( 16 )
#define MB_FUNC_READWRITE_MULTIPLE_REGISTERS ( 23 )
#define MB_FUNC_DIAG_READ_EXCEPTION ( 7 )
#define MB_FUNC_DIAG_DIAGNOSTIC ( 8 )
#define MB_FUNC_DIAG_GET_COM_EVENT_CNT ( 11 )
#define MB_FUNC_DIAG_GET_COM_EVENT_LOG ( 12 )
#define MB_FUNC_OTHER_REPORT_SLAVEID ( 17 )
#define MB_FUNC_ERROR ( 128 )
/* ----------------------- Type definitions ---------------------------------*/
typedef enum
{
MB_EX_NONE = 0x00,
MB_EX_ILLEGAL_FUNCTION = 0x01,
MB_EX_ILLEGAL_DATA_ADDRESS = 0x02,
MB_EX_ILLEGAL_DATA_VALUE = 0x03,
MB_EX_SLAVE_DEVICE_FAILURE = 0x04,
MB_EX_ACKNOWLEDGE = 0x05,
MB_EX_SLAVE_BUSY = 0x06,
MB_EX_MEMORY_PARITY_ERROR = 0x08,
MB_EX_GATEWAY_PATH_FAILED = 0x0A,
MB_EX_GATEWAY_TGT_FAILED = 0x0B
} eMBException;
typedef eMBException( *pxMBFunctionHandler ) ( UCHAR * pucFrame, USHORT * pusLength );
typedef struct
{
UCHAR ucFunctionCode;
pxMBFunctionHandler pxHandler;
} xMBFunctionHandler;
/* An array of Modbus functions handlers which associates Modbus function
* codes with implementing functions.
*/
static xMBFunctionHandler xFuncHandlers[MB_FUNC_HANDLERS_MAX] = {
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED > 0
{MB_FUNC_OTHER_REPORT_SLAVEID, eMBFuncReportSlaveID},
#endif
#if MB_FUNC_READ_INPUT_ENABLED > 0
{MB_FUNC_READ_INPUT_REGISTER, eMBFuncReadInputRegister},
#endif
#if MB_FUNC_READ_HOLDING_ENABLED > 0
{MB_FUNC_READ_HOLDING_REGISTER, eMBFuncReadHoldingRegister},
#endif
#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED > 0
{MB_FUNC_WRITE_MULTIPLE_REGISTERS, eMBFuncWriteMultipleHoldingRegister},
#endif
#if MB_FUNC_WRITE_HOLDING_ENABLED > 0
{MB_FUNC_WRITE_REGISTER, eMBFuncWriteHoldingRegister},
#endif
#if MB_FUNC_READWRITE_HOLDING_ENABLED > 0
{MB_FUNC_READWRITE_MULTIPLE_REGISTERS, eMBFuncReadWriteMultipleHoldingRegister},
#endif
#if MB_FUNC_READ_COILS_ENABLED > 0
{MB_FUNC_READ_COILS, eMBFuncReadCoils},
#endif
#if MB_FUNC_WRITE_COIL_ENABLED > 0
{MB_FUNC_WRITE_SINGLE_COIL, eMBFuncWriteCoil},
#endif
#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED > 0
{MB_FUNC_WRITE_MULTIPLE_COILS, eMBFuncWriteMultipleCoils},
#endif
#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED > 0
{MB_FUNC_READ_DISCRETE_INPUTS, eMBFuncReadDiscreteInputs},
#endif
};
eMBErrorCode eMBPoll( void )
{
static UCHAR *ucMBFrame;
static UCHAR ucRcvAddress;
static UCHAR ucFunctionCode;
static USHORT usLength;
static eMBException eException;
int i;
eMBErrorCode eStatus = MB_ENOERR;
eMBEventType eEvent;
/* Check if the protocol stack is ready. */
if( eMBState != STATE_ENABLED )
{
return MB_EILLSTATE;
}
/* Check if there is a event available. If not return control to caller.
* Otherwise we will handle the event. */
if( xMBPortEventGet( &eEvent ) == TRUE )
{
switch ( eEvent )
{
case EV_READY:
break;
case EV_FRAME_RECEIVED:
eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
if( eStatus == MB_ENOERR )
{
/* Check if the frame is for us. If not ignore the frame. */
if( ( ucRcvAddress == ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
{
( void )xMBPortEventPost( EV_EXECUTE );
}
}
break;
case EV_EXECUTE:
ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];
eException = MB_EX_ILLEGAL_FUNCTION;
for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )
{
/* No more function handlers registered. Abort. */
if( xFuncHandlers[i].ucFunctionCode == 0 )
{
break;
}
else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
{
eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
break;
}
}
/* If the request was not sent to the broadcast address we
* return a reply. */
if( ucRcvAddress != MB_ADDRESS_BROADCAST )
{
if( eException != MB_EX_NONE )
{
/* An exception occured. Build an error frame. */
usLength = 0;
ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
ucMBFrame[usLength++] = eException;
}
eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );
}
break;
case EV_FRAME_SENT:
break;
}
}
return MB_ENOERR;
}
再往下面就是一些奇技淫巧,一般这样写可能会被人打死。在32位系统下,把一个32位的变量当指针用。
/* 累加函数,给传入的a+1,传入的b+2 */
void Add(uint32_t a, uint32_t b)
{
*((uint8_t *)a) += 1;
*((uint8_t *)b) += 2;
}
int main(void)
{
uint8_t a = 0, b = 0;
add(&a, &b);
/* 打印结果a = 1, b = 2 */
printf("a = %d, b = %d\n", a, b);
}