总结一些日常工作常见的代码和编程情况,会分几个文章写出,也许对一些朋友有些作用
一、基础风格
1、设计函数参数时,请注意函数参数的顺序和个数
*一般而言,建议输入参数在前,输出参数在后
*输入参数是指针,且不改变时,请注意定义的时候类型加const
*输入参数是struct或者Class实例且不修改时,可以使用const&来传递,或者const指针
**这样可以避免当传入形参的实例对象较大时,拷贝会降低效率
**还可以避免对该对象的改变
*尽可能的减少不必要的参数个数
2、避免不必要的函数调用
*不合理的代码:
char* pszString = “hello, it's me”;
for(int i = 0;i < strlen(pszString); ++i)
{
。。。DoSomething
}
*合理的代码
char* pszString = “hello, it's me”;
int nLength = (int)strlen(pszString);
for(int i = 0;i < nLength; ++i)
{
。。。DoSomething
}
3、避免头文件定义全局变量,防止重复引用头文件时,全局变量被重复定义;
4、字符串拼接时请注意避免越界
1)因为有时候文件存储的文件名字段可能是错的,避免异常情况下的越界,拼接时需要注意是否越界
2)snprintfs的count参数请用_TRUNCATE,避免buffer太小导致的宕机(针对windows的PC)
https://msdn.microsoft.com/en-us/library/ms175769.aspx
5、如果处理资源出错,需要写日志时,请写明出错的文件名
6、继承接口或抽象类的子类,析构函数请注意定义为virtual
*当指针类型是父类指针,指向的内容是子类对象时,定义为virtual可以避免delete该指针时会出现泄露的情况
7、DWORD/long和void*转换
*64位的程序下,void*是8字节,DWORD/long是4字节。这样如果是指针存储在DWORD,高位的4个字节是会丢失
8、推荐调用函数时,对返回值进行判断,同时对错误的返回值做相应的错误处理
*确认是否需要函数返回值?
*确认出错了怎么办?错误如何表达?
*是否需要定义出错处理的准则或出错后是否需要还原?
二、断言的使用
1、static_assert执行编译时的断言检查
如:static_assert(sizeof(char *) == 8, "不是64位模式")
2、断言来检查参数合法性的场景:
1)代码执行之前或者在函数入口处,用断言来检查参数的合法性
2)代码执行之后或在函数出口处,用断言来检查是否正确执行,输出或内部状态是否如预期
3、避免用断言去检查程序错误:
1)外部不可靠的数据,应该做严格检查才能放到系统内部,这个时候它是守卫,提前检验和过滤不合理的数据和参数,应该使用错误检查处理代码,而不是用断言来做检查
*外部不可靠的数据:如不合理的用户输入、或其他模块传入到该模块的消息或数据,
有点像是对项目组外的员工,要动用项目组的资源,是需要经过审核的
2)系统内部的交互数据(如程序内部的调用),可以用断言来检查意想不到的错误,或者程序内部的假设。(其实就是检查它的潜规则和逻辑边界、隐形假定)
*因为系统内的调用者一般情况下是有义务负责传递给自己内部的数据是合理正常的数据
就像项目内的员工,对于使用项目组的资源的门槛就会低很多
*可理解为assert和出错处理是对所写程序建立不同的信用级别,也方便在Release版可以性能更好的运行程序
3)推荐针对public的函数或接口的入口处对参数做严格的错误检查;对于Private的函数或者只有这个模块能看到的,可以用assert
4、避免断言中使用改变环境的语句:
如不正确的代码:
int Test(int i)
{
assert(i++); //debug版和release版的i值就会不一样
return i;
}
int main()
{
int i = 1;
int nValue = Test(i);
printf("%d\n", nValue);
return 0;
}
合理的形式:
int Test(int i)
{
assert(i);
return ++i;
}
注:与改变环境的语句类似的行为是宏定义。
*请尽量不要在assert中调用宏,以防止宏的副作用
5、防错性程序设计常常要解决的是:现实中,防止用户数据丢失或程序崩溃而采取的措施。只是除此之外:
*我们是否还希望进行防错性程序设计时,错误不要被隐瞒?
*实现程序时,我们想要什么?我们期待错误发生时,我们能得到什么或者怎样处理它?
**这有点像我们接到一个任务的思考点:
1)我们做这个事的目标是完成它?做好它?
2)做这个事是希望减轻自己负担?减轻别人负担?or both?
3)出问题时怎么办?有预案吗?有线索吗?系统里需要埋下什么报警方案?影响大吗?
三、内存
1、内存常见的设计问题导致的代码bug
1)内存的分配、释放接口应配对;且应该限定在一个同一模块或者同一抽象层内进行
*否则会加大程序猿追踪内存块的生命周期的负担
*还可能导致内存泄露、重复释放该内存、非法访问已经释放的内存、写入已经释放或者没有分配的内存等问题
例:
**不正确的代码:
#define MIN_MEM_SIZE 10
int CompareMemorySize(char* pchBuffer, size_t uSize)
{
if(uSize < MIN_MEM_SIZE)
{
free(p);
p = NULL;
return -1;
}
return 0;
}
void AllocMemory(size_t uSize)
{
char *pchBuffer = (char*)malloc(uSize);
if(pchBuffer == NULL)
{.........}
if(CompareMemorySize(pchBuffer, uSize) == -1) //这里pchBuffer实际上是已经释放了
{
free(pchBuffer);//重复释放了pchBuffer
pchBuffer = NULL;
return;
` }
........
free(pchBuffer);
pchBuffer = NULL;
}
**正确的代码:
#define MIN_MEM_SIZE 10
int CompareMemorySize(char* pchBuffer, size_t uSize)
{
if(uSize < MIN_MEM_SIZE)
{
return -1;
}
return 0;
}
void AllocMemory(size_t uSize)
{
char *pchBuffer = (char*)malloc(uSize);
if(pchBuffer == NULL)
{.........}
if(CompareMemorySize(pchBuffer, uSize) == -1)
{
free(pchBuffer);
pchBuffer = NULL;
return;
` }
........
free(pchBuffer);
pchBuffer = NULL;
}
**这里可以总结出的维度:
(1)内存的分配、释放接口应配对;且应该限定在一个同一模块或者同一抽象层内进行
(2)尽可能函数的副作用越少越好:减少用户记住调用函数时,需要了解过多的函数内部的实现
2、避免对结构体执行逐个字节的比较
如:
typedef struct
{
char chValue;
int nCount;
char szBuffer[10];
}Buffer;
*不合理的代码:
bool IsEqual(const Buffer *pBuffer1, const Buffer *pBuffer2)
{
//因为默认情况想存在字节对齐,对齐的字节内存里填充什么内容和编译器有关系,它的行为未定义,所以这样比较是错的
if(!memcmp(pBuffer1, pBuffer2, sizeof(Buffer)))
return true;
return false;
}
*合理的代码:
bool IsEqual(const Buffer *pBuffer1, const Buffer *pBuffer2)
{
if(
pBuffer1->chValue == pBuffer2->chValue &&
pBuffer1->nCount == pBuffer2->nCount &&
` strcmp(pBuffer1->szBuffer, pBuffer2->szBuffer) == 0
)
return true;
return false;
}
3、避免执行零长度的内存分配
*C99规定,程序试图调用malloc、calloc与realloc等系列内存分配函数分配大小为0的内存,其行为时有具体编译器所定义(可能返回NULL,可能返回长度为非零的值等),从而导致产生未定义的行为
4、内存常见检测问题:
1)确保没有访问空指针
2)分配和释放内存的操作是否配对
3)出错处理时,内存是否有出现重复释放、漏释放、或者释放错误
4)在指针赋值前,是否存在有内存数据丢失
5)当释放数组元素或者struct类型的元素时,都应先遍历子元素进行释放,再遍历回父节点
*注:如果class或者一些设计不好的接口,没有注意提供配对的内存操作接口时,会导致各种隐患,如:内存泄漏、野指针等,需要另外检查
6)注意对函数返回值是动态分配内存的检查和内部状态的检查
7)引用计数的检查
8)内存填充是否越界、或数据类型不正确