目录
一.编写背景
作为一个C语言软件开发程序员,我们应该具有良好的代码编写习惯,这样可以在保障代码质量的前提下,大幅度地缩短需求开发和后期维护的时间。而C语言的代码规范大致分为以下两个方面:安全性,可维护性和可读性。
代码规范要求 | 详细解释 | 最终目的 |
安全性 | 缺陷性顾名思义指的是代码缺陷的数量,代码出现的缺陷越少,则越不容易产生故障,不容易让代码跑飞。 | 消除需求功能的缺陷。 |
可维护性和可读性 | 可维护性和可读性指的是,在需求功能完成之后,后续维护代码的难度。可维护性越好,后续代码维护的速度越快。可读性越好,理解代码的所用的时间越少。 | 为后续维护节省大量时间。 |
二.代码规范
如上面所说,代码规范分为安全性,可维护性和可读性,因此针对以上两个专题进行学习和改进。
1.安全性规范
1.1 禁止数组越界
在C语言中,数组越界是最常见的一种代码缺陷,主要包括数组索引越界和数组值越界,这种缺陷会导致底代码段错误。
不规范代码示例:
typedef unsigned char U8
#define MAX_INDEX 10
U8 arr[MAX_INDEX]={0};
//数组索引越界
arr[15]=0;
//数组值越界
arr[0] =256;
这段简单代码的功能是将大小为10的unsigned char类型的数组初始化后,对其元素进行赋值,同时展示了两个问题、
(1)对索引为15的元素进行了赋值,大于规定的最大索引10,造成数组索引越界的问题。
(2)对数组元素赋值256,而unsinged char 的取值范围是0到255,造成数组值越界的问题。
1.2 禁止空指针解引用
在C语言中,禁止对空指针解引用,对一个空地址的指针赋值会造成段错误。而避免空指针解引用的方法之一就是在函数入参前对其进行判空断言,如下面代码所示。
不规范代码示例:
#include<assert.h>
void test_value(unsinged char *ptr)
{
//进行后续操作
.....
}
规范代码示例:
#include<assert.h>
void test_value(unsinged char *ptr)
{
assert(NULL == ptr);
//进行后续操作
.....
}
1.3 避免返回局部变量
在C语言中避免将局部变量作为返回值返回,因为局部变量存储在栈区,函数运行结束后栈上存储的局部变量就会被释放,
原则上除局部变量的内存地址不能作为函数的返回值外,其他类型的局部变量都能作为函数的返回值。但是及时上不建议这么使用。
不规范代码示例:
unsinged int test_code(unsinged int para)
{
unsinged int value = para;
return value;
}
1.4 避免野指针的产生
在C语言中,应该尽量的避免野指针,因为野指针的地址是不确定的,对于不确定的地址解引用也会造成段错误。而野指针的产生主要有以下两种途径
(1)在指针定义后未进行初始化就进行解引用,如下面代码所示,ptr是野指针,对其解引用会造成代码错误。
不规范代码示例:
//指针初始化
unsinged int* ptr;
规范代码示例:
//指针初始化
unsinged int* ptr = NULL;
(2)在动态分配内存后未对使用后的内存进行释放,也会造成野指针的错误。
不规范代码示例:
unsinged int *ptr = NULL;
//动态分配内存
ptr = (unsinged int *)malloc(sizeof(unsinged int*));
//判空
if(NULL == ptr)
{
....
}
//初始化内存值
memset(ptr, 0, sizeof(unsinged int *));
//后续使用
......
规范代码示例:
unsinged int *ptr = NULL;
//动态分配内存
ptr = (unsinged int *)malloc(sizeof(unsinged int*));
//判空
if(NULL == ptr)
{
....
}
//初始化内存值
memset(ptr, 0, sizeof(unsinged int *));
//后续使用
......
//使用结束后释放内存
free(ptr);
//将不用的指针指向NULL
ptr = NULL;
总的来说,野指针的避免方法如下
(1)在指针初始化的时候将其指向NULL。
(2)在动态分配内存的指针使用结束之后,需要将内存释放并将指针指向NULL 。
1.5 避免非法的强制转换
在C语言中,在大范围的数据类型强制转换为小范围的数据类型时,可能会造成数据丢失。
不规范代码示例:
typedef unsinged char U8
tyoedef unsinged int U32
U32 value32 = 0xffff;
U8 value8 = (u8)value32;
在上面用例中将U32类型的值强制转换为U8类型,会导致高8位或低8位丢失,这个取决于大小端。因此我们要尽量避免从大范围的数据类型强制转换为小范围的数据类型。
1.6 避免直接操作指针
在C语言中,经常会出现函数中修改指针的情况,而一个指针往往不止在一个函数中调用,如果直接操作指针,会导致他指向的地址发生变化,从而使得其他函数调用指针时指针指向的地址不是我们期望的地址,用下面的例子来进行详细说明
不规范代码示例:
void test_code(unsinged int *ptr)
{
//直接操作ptr指针
....
}
规范代码示例:
void test_code(unsinged int *ptr)
{
unsigned int *Ptr = ptr;
//操作Ptr指针
....
}
1.7 正确使用断言
断言assert的作用是检测代码中的异常情况,并且只会在DEBUG版本中生效。但是需要注意断言和错误处理的区别,断言检测的是异常代码,也就是一旦出现异常整个程序就必须结束,而错误处理只是针对某个错误分支进行操作,并不会结束整个程序。
适合使用断言的情况:
include<assert.h>
void test_code(unsigned int *ptr)
{
//断言判空
assert(NULL != ptr);
//后续操作
....
}
适合使用错误处理的情况:
#define MALLOC_SUCCESS 0
#define MALLOC_FAIL 1
unsigned int test_code(void)
{
unsigned int *ptr = NULL;
ptr = (unsigned int*)malloc(sizeof(unsigned int*));
if(NULL == ptr)
{
//错误分支
return MALLOC_FAIL;
}
//正常分支
....
return MALLOC_SUCCESS;
}
注意上面的代码示例只是为了说明异常处理和错误处理的区别,正常在函数里给指针申请内存需要使用二级指针来实现。
1.8 头文件中使用条件编译
头文件中使用条件编译的目的是防止多个文件在调用头文件的时重复包含从而使编译失败。
规范代码示例:
//如果该头文件未被包含,就执行头文件的内容,已包含则直接跳过该头文件
#ifndef __ TEST_H__
//定义头文件相关的宏
#define __ TEST_H__
//执行头文件中的内容
...
#endif
2.可维护性和可读性规范
2.1 合理地加空格
为了增加代码的可读性,需要在函数参数后增加空格,如下面代码所示。
错误代码示例:
unsigned int *ptr=NULL;
void test_code(U32 para1,U32 para2,U32 para3);
正确代码示例:
unsigned int *ptr = NULL;
void test_code(U32 para1, U32 para2, U32 para3);
2.2 进行代码缩进
为了代码的可读性,可以对代码进行缩进,一般是四个空格,不建议使用Tab进行缩进,因为在某些 IDE 中Tab默认是八个空格。
2.3 避免魔鬼数字
在代码中尽量避免纯数字的使用,因为纯数字会影响代码的可读性,要尽可能的使用宏定义来替换。下面两种情况可以用于纯数字。
(1)在进行变量初始化的时候可以进行纯数字进行赋值。
(2)在对变量赋没有特定意义的值时可以使用纯数字。
不规范代码示例:
iic_read(0,0x2000,1);
规范代码示例:
#define DEV_ID 0 //设备ID
#define POR_ADDR 0x2000 //上电复位寄存器地址
#define POW_ON_RESET 1 //上电复位标志位
iic_read(DEV_ID, POR_ADDR, POW_ON_RESET);
2.4 规范变量的命名
C语言中变量的命名也非常地影响代码的可读性,通用的命名规则如下
(1)全局变量:以g_开头,后面紧跟功能名称。
//全局变量命名
unsigned int g_logicID
(2)函数命名:函数命名有两种命名方法,一种是小写字母加下划线,另外一种是驼峰式命名。
//小写字母加下划线函数命名
unsigned int get_logicId(void);
//驼峰式函数命名
unsigned int GetLogicId(void);
(3)局部变量命名:局部变量的命名方式和函数命名一致。
(4)宏定义命名:宏定义命名的方式是大写字母加下划线。
#define LOGIC_ID 0x01
#define LOGIC_ADDR 0X3024
(5)指针命名:指针命名一般用p开头,然后紧跟功能名称。
unsinged int *pAttr = NULL;
2.5 ==语句中将常量放在左边
在判断==语句当中,要习惯性的把常量放在左边,这样做的好处是如果误将相等符号 == 写成赋值符号 = 时,编译器会报错,反之则不会。
不规范代码示例:
if(ptr == NULL)
{}
规范代码示例:
if(NULL == ptr)
{}
2.6 尽量使用static修饰函数
如果全局变量和函数只在单个文件内进行调用,使用static来修饰函数和全局变量,这样使得作用范围更小,从而更安全。可以防止误调用
2.7 避免使用全局变量
尽量避免使用全局变量,因为全局变量可以被任何函数或模块修改,对于后期维护来说非常麻烦,必须要读写全局变量的话,建议使用函数进行封装。
2.8 括号成对出现并单独占一行
对于if,switch,whiile,for等语句括号必须成对出现并且单独占用一行。
2.9 避免使用extern
要调用别的文件的函数和全局变量时,尽量不要使用extern而使用头文件包含#include,因为使用extern声明函数时,它只会检查函数名,而不会去检查函数参数的数量和类型,即使无法正常调用,编译器也不会报错。所以尽可能的使用#include来替换extern。
不规范代码用例:
//这里函数参数就算写成int类型,或者新增参数数量,编译器都不会报错
extern void test_code(void);
规范代码用例:
#include "test.h"