【知识分享】C语言应用——指针篇

一、概述

    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);
}

五、相关链接

    C语言中的设计模式——表驱动模式
    C语言应用-易错篇

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识噬元兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值