C语言经验篇之编码总结


记录下以前工作中的一些编码心得,养成一个好的编码习惯是成为一个优秀程序员的基本素养。

char data[0]的用法及意义

在大型项目中常见到这样的结构体定义:

typedef struct
{
	int len;
	char data[0];
} Test_s;

首先可以明确的一点是:char data[0] 这个数组是没有元素的,它的地址紧跟着len后的地址,如果分配的内存大于结构体的实际大小,那么大出来的那部分就是data的内容。
这种写法可以用来形成动态缓冲区buff. 如此编码的好处在于:

  1. 节省空间,相较于用指针指向新开辟的内存区域的用法,data[0]是不占用内存的,而指针占用4字节内存大小。
  2. 便于内存缓冲区的管理。如果用指针char *代替 char data[0],则使用的时候需要两次分配内存(一次是malloc结构体,一次是malloc结构体中的char *指针),释放的时候也同样需要两次释放,如果一不小心漏了一次还容易造成内存泄漏,不够安全。
  3. 减少内存碎片化:结构体里嵌套指针首先使用的时候需要两次malloc,分配的内存是不连续的,这样本身就会造成碎片化,其次就是对齐问题也会造成内存碎片。而char data[0]的用法只需要malloc一次,free一次即可,分配的内存还是连续的。所以说减少了内存的碎片化

这种声明方法可以巧妙的实现C语言里的数组扩展,即我们常说的柔性数组,下文会详细解释什么是柔性数组。

magic数字

代码编写过程中比较忌讳出现神奇数字。原因如下:

  1. 大型项目中可能会出现多个函数或多个文件用到同一个判断条件,比如说:if (condition < 10),condition为全局变量,如果如刚才所写,当判断条件发生改变时很容易发生漏改的情况.
  2. 使用神奇数字不利于代码的可读性以及鲁棒性。

正确写法如下:

#define TOTAL_NUM  10u 
if (condition < TOTAL_NUM)
{
	;
}

代码封装

封装的好处:
1.代码简洁,易于阅读理解
2.便于解耦
在编写代码的过程中可以将功能性代码封装成一个函数,而不是写一长串的代码。当代码超过40行时就已经不利于阅读者阅读了,此时就要考虑是否可以将代码片段封在函数内,当然也不能为了封装而封装,如果封装的代码没有一点关联,这样的封装也没有意义。正常情况下当我们写一个for循环进行阻塞时,就不如将其封成一个my_delay函数,这样阅读者一看函数名称就知道这里边是干嘛的。

void my_delay(int ms)
{
	U32 tick = get_time();
	while(get_time() - tick < ms);
}

还有一种封装,对于内部需要使用的接口函数,可以通过结构体封装函数指针的方式来实现函数api的封装,比如说:

void my_handleDataCbFunc(U8 len, U8 *data);

typedef void (*HandleDataCb)(U8 len, U8 *data)
typedef struct
{
	HandleDataCb handleDataCbFunc;
	U8 len;
	U8 data[0];
} HandleDataCallouts;
const HandleDataCallouts handleData =
{
	.handleDataCbFunc = &my_handleDataCbFunc,
	.len = 100,
	.data = "hello"
};

以及一些方法的封装:

//仅仅举例,函数的形参略去。
static int spi_init();
static int spi_trans();
static int spi_control();
static int spi_deinit();
//spi_method 定义方法同上边的一个例子
static spi_method spiBusOption =
{
	.init = spi_init,
	.trans = spi_trans,
	.control = spi_control,
	.deinit = spi_deinit
}

堆泄露,栈溢出

每个项目中都会存在这样一个函数用来获取当前内存使用情况。当发生堆泄露,栈溢出的情况时往往伴随着assert死机,这种问题调试起来比较麻烦,比较笨的方法就是在存在内存分配的task结束的地方调用获取内存资源的函数,去查看具体是哪个task发生的泄露,然后再具体去查看是task里哪个地方.
引发堆泄露,栈溢出的原因很多,具体信息详见下文的内存使用常见问题的章节

状态机

当一个事件有多种状态时,通过不同的状态判断进行分门别类的处理,使用状态机处理事件会让整个代码框架看起来简洁明了,更有层次感。在项目中状态机被使用的频率相当之高。
状态机具体介绍详见下文:(待后续补充)
状态机sample code 参考:(待后续补充)

定时器的使用

定时器分为软,硬两种定时器。
软定时器:

  1. 不要在定时器里做过于耗时的处理。不必要的定时器,启动后,需要注意释放。
  2. 不要在中断里添加和启动软定时器,因为软定时器是由线程管理,比较耗时。

硬定时器就是利用Timer硬件寄存器来获取时间,精度较高

中断

中断是个大话题,本节只是简单记录下边沿中断需要多考虑的地方,后续可能会专门开一个文章讲述中断。

对于双边沿触发的中断或者边沿触发的中断,在使用的时候需要增加滤波处理,以防止误判或者误触发的情况,
每隔20ms去get一次pin脚状态,大概3次左右,去判断当前状态是否是期望的高或低的状态即可。task之间交互可以通过使用消息队列。
具体做法思路如下:

  1. 起一个滤波处理任务
  2. 在每次中断回调里边发送消息给处理任务,并将当前pin脚作为参数,传参。用以区分当前是那个引脚,需要去做什么。

具体操作方法以IO中断见如下代码片段:

 /* 以下示例涉及的具体数值仅供参考 */
#define OK   					0 //成功
#define NOT_OK   				1 //失败

#define FILTER_TASK_SIZE		1024
#define FILTER_TASK_PRIORITY	15
#define QUEUE_MAX_CNT			15
#define PIN_READ_CNT			3
#define PIN_HIGH_VALID			1
#define PIN_LOW_VALID			0
#define PIN_HIGH				1
#define PIN_LOW					0
#define INVALID_PIN				3
static queueHandle ioFilterQueue = NULL; //io滤波队列句柄
static threadHandle ioFilterThread = NULL; //task句柄
/**
 * @brief 滤波函数,用以获取当前pin脚状态
 * @param
 * /
U8 IO_filterHandle(void *param)
{
	U8 cnt = 0;
	U8 pinStatus = 0;
	U8 validLevel = INVALID_PIN;
	pin_info_s pin = *((pin_info_s *)param);
	while(1)
	{
		if(pin_read(pin)) //读取当前引脚状态,如果为高,将pinStatus置为1
		{
			pinStatus = (pinStatus << 1) | 0x01;
		}
		else
		{
			pinStatus = pinStatus << 1;
		}
		cnt++;
		if(cnt >= PIN_READ_CNT)
		{
			if(pinStatus == PIN_HIGH_VALID)
			{
				validLevel = PIN_HIGH; //高电平
			}
			else if(pinStatus == PIN_LOW_VALID)
			{
				validLevel = PIN_LOW;
			}
			cnt = 0;
			pinStatus = 0;
			break;
		}
		sleep(20); //系统睡眠20ms
	}
	return validLevel;
}
/**
 * @brief 滤波中断处理函数
 * @param
 * /
void IO_pinFilterIrqHandle(pin_info_s pin, U8 level)
{
	if(level == INVALID_PIN)
	{
		//打印无效的pin脚状态
	}
	/* 根据不同的pin脚的电平状态,来处理具体的事件。可以通过队列来将事件发出,在另外的层去接收事件,并实现具体的事件 */
	switch(pin)
	{
		/* 比如其中一个pin脚是用做主电源掉电检测的 */
		case POWER_OFF_DETECT_PIN:
			if(level == PIN_LOW)
			{
				send_event_message(VBAT_POWER_OFF_EVENT);
			}
			else
			{
				send_event_message(VBAT_POWER_ON_EVENT);
			}
			break;
		default:
			break;
	}
}
 
/**
 * @brief 滤波处理task
 * @param
 * /
void IO_filterHandleTask(void *param)
{
	pin_info_s pin;
	U8 ioLevel;
	U32 timeout = 0xFFFF;
	while(1)
	{
		if(queue_fetch(ioFilterQueue, &pin, timeout) == OK)
		{
			ioLevel = IO_filterHandle(&pin);
			IO_pinFilterIrqHandle(pin, ioLevel);
			IO_pinIrqEnable(pin); //使能中断
		}
	}
}

/**
 * @brief 滤波事件通知函数
 * @param
 * @note 该函数在IO中断函数里调用,用以将触发中断的pin脚,作为入参传进消息队列中。
 * /
int IO_filterEventNotify(pin_info_s pin, U32 timeout)
{
	/* 用于将消息内容发送到指定消息队列,入参:消息队列句柄,消息指针,超时时间 */
	queue_post(ioFilterQueue, &pin, timeout);
}

/**
 * @brief 初始化函数
 * @param
 * @note 该函数用以完成消息队列的初始化,滤波task创建,以及IO脚的中断配置
 * /
int IO_init(void)
{
	int optRetVal = NOK;
	/* 用于创建并初始化消息队列,入参:消息队列句柄,消息大小,消息个数 */
	bool retValue  = queue_creat_init(&ioFilterQueue,sizeof(pin_info_s),QUEUE_MAX_CNT) == OK;
	
	/* 用于创建task,入参:一般为task句柄,name, 大小, 优先级,task函数,task函数入参(可以为NULL) */
	retValue  = (thread_creat(&ioFilterThread, "IO_filterHandleTask", FILTER_TASK_SIZE, FILTER_TASK_PRIORITY, IO_filterHandleTask, NULL) == ok) && retValue;
	if (retValue)
	{
		/* 初始化Io脚,配置中断方式 */
	}
	else
	{
		queue_free(ioFilterQueue); //释放消息队列
	}
	return optRetVal;
}

关于Malloc

小内存或者可以明确需要使用多少字节的不要使用Malloc去动态申请内存,因为动态申请内存容易产生碎片。
而且,值得注意的一点就是动态内存申请,在其不再需要使用的时候,必须free掉,否则即使是再小的内存申请,只要程序不退出,长时间的累加也会出现内存泄漏,从而导致系统崩溃。
但是为什么会出现内存碎片呢,浅见如下:分布式内存管理由内存池和内存管理表两部分组成。内存池被等分为N块,对应的内存管理表,大小也为N,内存管理表的每一项对应内存池的一块内存。那么这里有个问题,假如我申请97个字节,那我就得申请97%32(视处理器体系结构而定)+1个内存块,就会出现其中一个字节占用了一个内存块的情况,这就形成了内存碎片。

另外malloc在使用过程中还得注意判断内存是否申请成功,而且内存释放后(使用free函数之后指针变量p本身保存的地址并没有改变),需要将p的值赋值为NULL(拴住野指针)。

char *p;
p=(char *)malloc(256);
free(p);
p =NULL;

C中内存位置浅述

栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等,栈地址是不固定的。栈是向着内存地址减小的方向生长
堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆是向着内存地址增加的方向生长。
自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局存储区(静态存储区):全局变量和静态变量的存储是放在一块的,位置是固定的。初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

数组

数组初始化
  1. 对于全局数组不论是否初始化,默认值都为0
  2. 对于局部数组,如果没有初始化,默认值为随机值,因为局部数组作用域为栈空间,我们声明数组,其实只是移动栈顶指针.而栈内的数据是上一次出栈时候遗留的数据.所以当一个局部变量未初始化时,它的值可能是随机的。个人浅见就是,如果是一片从未使用过的原始的栈空间,那么里边的值应该是0.但是不能保证的是从软硬件开发出来后,谁能保证它没有运行过呢,毕竟要进行测试的,当运行过后,这一片栈的空间的栈顶指针所指向的数据是什么就不可知了。
动态数组

在通常情况下,如果想要高效的利用内存,那么在结构体内部定义静态的数组是非常浪费的行为。此时就需要在结构体中存放一个长度动态的字符串。一共有两种方式:

  1. 普通方式
    在结构体中定义一个指针成员,这个指针成员指向该字符串所在的动态内存空间。
    struct MyData
    {
    int nLen;
    char *p;
    };
  2. 柔性数组方式
    由于C语言中结构体的最后一个元素可以是大小未知的数组,也就是所谓的0长度,柔性数组成员必须定义在结构体的最后一个,并且不能是唯一的成员。
    struct MyData
    {
    int nLen;
    char data[];
    };

内存使用常见问题

内存主要包含:只读数据区,静态数据区,堆区及栈区空间。数据区内存在程序编译时分配。函数执行时在栈上自动开辟局部变量的储存空间,执行结束时自动释放栈区内存。堆区内存亦称动态内存,程序在运行时调用库函数申请,释放同样要显示的调用对应库函数释放

只读数据区

写操作时会报内存操作错误。

数据区内存

内存越界

  1. 内存拷贝时超过目标大小
  2. 写操作时超过数组范围
    所以,进行读写操作时,注意结束符及内存大小
栈区内存
  • 内存未初始化

    栈区定义的变量如果未初始化,指向的为随机值。直接使用会导致预期外的结果。尤其是指针未被初始化时如果直接访问,很可能会篡改别的内存中存储的信息。

    所以,在栈区定义变量时注意赋初值

  • 栈内存溢出

    多任务环境下,栈区开辟空间小于任务运行时消耗的空间时,会导致程序崩溃。
    所以,预估任务栈消耗资源,合理分配大小。
    通常在一个项目工程中会设计一个代码接口,用与查看任务栈消耗了多少资源
    比如说thread_get_watermark。

  • 使用返回的栈地址

    函数内的局部变量在函数运行结束时会被释放。
    所以,不要用return语句返回指向栈内变量的指针,也不要将指向栈内变量的指针作为异步程序的参数进行引用。
    比如说音频播放函数里有一个回调函数,该回调函数使用的时候传参传入了该播放函数里的栈区指针,这样就可能出现一种情况就是在进入回调函数时该栈区空间已经释放,这样做就会导致程序崩溃。

堆区内存
  • 内存未初始化

    通过malloc库函数分配的内存,初始值是未定义的,如果直接操作,操作其实是一些垃圾值,当这些垃圾值参与到程序的逻辑控制时,会引发逻辑控制预期外的结果。

    所以,在malloc后,最好使用memset对其清0。

  • 内存释放和分配不匹配

    malloc与free配套使用。不同的使用者对于内存分配与释放的函数封装实现不一样。不要觉得这个问题很小,很容易。但是也很容易被忽视,这是本人在项目工作中真实遇到过的,当时查找系统崩溃的原因时找了好久才定位到这个原因。

    所以,在选择分配和释放的接口调用时必须一一对应,防止内存泄漏。比如说:osiMalloc->osiFree,Malloc->Free,
    malloc->tfree

  • 堆内存泄漏

    堆内存泄漏指的是,由于疏忽或者错误造成已不再使用的内存没有被释放。由于任务分配的有内存大小,所以少量的泄漏暂时可能不会导致什么问题,但是当程序持续运行的时候,泄漏的内存会持续消耗过多内存,导致程序最终崩溃。

常见的造成内存泄漏原因:

  1. 已申请内存的指针被用来指向别的内存区
  2. 由于函数提前退出导致内存未被释放
  3. 试图通过函数指针参数申请并传递动态内存
  4. 多任务处理时,内存分配与释放不在同一处。由于流程过于复杂,可能遗忘释放等

关于指针

野指针
  • 指针变量未初始化

    任何指针刚被创建的时候不会自动成为NULL指针,它的缺省值是随机的。所以指针变量在创建的时候应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  • 指针释放后未置空

    指针在free或者delete 后未赋值为NULL,因为free只是把指针所指的内存给释放掉,此时指针指向的就是”垃圾”内存,所以释放后的指针应立即置为NULL

  • 指针操作超越变量作用域

    这种情况是指的调用返回指向栈内存的指针,因为栈内存会在函数结束时释放内存

末尾附一张C语言涉及内存操作的常用库函数思维导图。

持续更新中。。。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值