C语言代码评审小结

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 最近,我参加了几个C语言代码的评审会,发现了代码中一些共同存在的问题,特总结下来,供相关的开发人员参考。

概述
在实际的软件开发项目中,代码评审是一个必不可少的流程。代码评审,也称之为代码复查,是指通过阅读开发人员所写的代码来检查源代码与编码规范的符合性以及代码质量的活动。总的说来,代码评审的好处有以下几点:
第一,发现程序问题,提高代码质量。
第二,理清代码逻辑,开阔编程思路。
第三,促进团队交流,提升开发技能。

代码评审的大体流程是这样的:
第一步,团队负责人(通常是开发经理)提前预定好会议室,并通知参与代码评审的人员,让他们做好准备。
第二步,在评审会上,讲解员大声阅读被评审的代码,评审人员就代码的问题给出自己的看法和意见,记录员再将这些意见记录下来。
第三步,评审结束之后,团队负责人将评审记录以邮件的形式发出来,并督促被评审代码的编写者按照评审意见对代码进行修改。

代码评审小结
最近,我参加了几个C语言代码的评审会,发现了代码中一些共同存在的问题,特总结下来,供相关的开发人员参考。

1.添加、修改、删除代码的时候缺少了注释说明。
在很多开发人员看来,注释是可有可无的东西,只要实现了程序的功能即可。实际上,这种想法是不对的。对于缺少必要注释的代码,在刚编写的时候,还能够知道每行代码的作用,但是当经过了多个版本的演进之后,不要说其他人,就算是作者本人,也未必知道当初自己为何要这么编写代码。当你接手缺少注释而读起来很吃力的代码的时候,有一种想把代码原作者拉过来打一顿的冲动。不管你又没有,反正我是有的。

因此,不管工作有多忙,在编写代码的同时,我们一定要在适当的地方添加适当的注释,这也是对一个合格的编程人员的基本要求。

2.在代码关键分支处缺少了日志信息,不便于分析和排查问题。
很多代码在“return”语句之前或else分支处没有日志信息,这使得跟踪代码流程变得比较困难,在程序出现故障的时候不便于分析和排查问题。

例如,如下代码:

if (var1 > 0)
{
    return;
}
if (var2 > 0)
{
    return;
}
if (var3 > 0)
{
    return;
}

当程序执行到这段代码而返回的时候,我们不知道到底是哪个变量大于了0,这导致我们不知从哪里入手排查问题。

正确的做法是在每个“return”语句之前,都添加详细的日志说明,如下语句所示:

if (var1 > 0)
{
    // 日志说明1
    return;
}
if (var2 > 0)
{
    // 日志说明2
    return;
}
if (var3 > 0)
{
    // 日志说明3
    return;
}

又如,如下代码:

if (var1 > 0)
{
    // 执行操作1
}
else
{
    return;
}
if (var2 > 0)
{
    // 执行操作2
}
else
{
    return;
}
if (var3 > 0)
{
    // 执行操作3
}
else
{
    return;
}

当程序执行到这段代码而返回的时候,我们不知道到底是在哪个else分支返回的。正确的做法同上,要在每个“return”语句之前,都添加详细的日志说明。

3.代码中出现了魔幻数字,不知道它们表达的意思。
例如,如下代码:

if (var1 == 100)
{
    // 执行操作1
}
else if (var1 == 200)
{
    // 执行操作2
}
else if (var1 == 300)
{
    // 执行操作3
}
else
{
    // 执行操作4
}

在看到这段代码的时候,我们不知道代码中的100、200、300表示什么意思,只是感觉头有点晕。这类我们不知道表达什么意思的数字就统称为魔幻数字。

为了消灭魔幻数字,正确的做法是将它们用宏替代,以提高代码的可阅读性和可修改性。将上面的代码修改之后如下所示:

#define   SMALL    100
#define   MEDIUM   200
#define   BIG      300

if (var1 == SMALL)
{
    // 执行操作1
}
else if (var1 == MEDIUM)
{
    // 执行操作2
}
else if (var1 == BIG)
{
    // 执行操作3
}
else
{
    // 执行操作4
}

4.在使用字符串相关操作函数strcpy、memcpy等的时候,没有判断拷贝长度。
在涉及到字符串相关操作的时候,大家一定注意防止操作不当而引起内存越界。如下代码没有判断拷贝长度,就容易引起了内存越界:

char src[100] = {0};
char dest[80] = {0};

strcpy(dest, src);

如上面的代码所示,当src中字符串的长度大于了80的时候,就会出现内存越界的情况,程序就会崩溃。

正确的做法是在执行拷贝操作之前,先判断字符串的长度,同时将strcpy函数替换为strncpy。修改之后的代码如下所示:

char src[100] = {0};
char dest[80] = {0};
int  len      = 0;

if (strlen(src) >= sizeof(dest)-1)
{
    len = sizeof(dest)-1;
}
else
{
    len = strlen(src);
}
strncpy(dest, src, len);

5.if…else语句的判断条件不明确,用多个变量作为多个else if的判断条件。
例如,如下代码所示:

#define   SMALL    100
#define   MEDIUM   200
#define   BIG      300

if (var1 == SMALL)
{
    // 执行操作1
}
else if (var1 == MEDIUM)
{
    // 执行操作2
}
else if (var2 == BIG)
{
    // 执行操作3
}
else if (var3 == BIG)
{
    // 执行操作4
}
else
{
    // 执行操作5
}

在上面的代码中,if…else语句的判断条件中使用了三个不同的变量var1、var2和var3,这导致了代码阅读起来不方便,而且后续对代码的修改也容易出现错误。

正确的做法是同一级的if…else语句的判断条件中要使用相同的变量,修改之后的代码如下所示:

#define   SMALL    100
#define   MEDIUM   200
#define   BIG      300

if (var1 == SMALL)
{
    // 执行操作1
}
else if (var1 == MEDIUM)
{
    // 执行操作2
}
else
{
    if (var2 == BIG)
    {
        // 执行操作3
    }
    else
    {
        if (var3 == BIG)
        {
            // 执行操作4
        }
        else
        {
            // 执行操作5
        }
    }
}

6.定义变量的时候排版不工整。
在编写几乎每一个函数的时候,我们都会定义一些变量,包括整型变量、字符型变量、指针变量、结构体变量等。如果把这些变量放在一起而不注意排版,那么看起来会非常的乱。如下代码所示:

char str1[50] = {0};
int i = 0;
int max = 0;
char str2[500] = {0};
char *pStr = NULL;
TestStruct_T tTestStruct = {0};
int j = 0;
char str3[1000] = {0};

从上面的代码可以看出,将不同的变量混合放在一起,代码看起来很凌乱,严重影响了程序的可阅读性。为此,我们要将同种类型的变量放在一起,利用缩进来让它们对齐,并利用空行来分隔不同类型的变量。修改之后的代码如下所示:

int i   = 0;
int j   = 0;
int max = 0;

char str1[50]   = {0};
char str2[500]  = {0};
char str3[1000] = {0};

char *pStr = NULL;

TestStruct_T tTestStruct = {0};

这样修改之后的代码阅读起来,层次感更好,可阅读性更强。

7.没有判断函数输入参数(尤其是指针变量)的合法性。
在定义函数的过程中,我们很容易忘记对它的输入参数的合法性进行校验,这样也就增大了程序出问题的风险。如下代码所示:

void TestFun(char *pMsg)
{
    char buf[1000] = {0};

    memcpy(buf, pMsg, sizeof(buf)-1);

    // 执行后续流程
}

可以看到,在函数TestFun中,我们直接将pMsg拷贝到buf中,而没有判断其是否为空。如果pMsg为空,那么这个拷贝就会出现问题,程序就会崩溃。

正确的做法是在使用输入指针pMsg之前,先对其合法性进行校验,也就是判断它是否为空。如果为空,那么直接返回,不进行后续处理。修改之后的代码如下:

void TestFun(char *pMsg)
{
    char buf[1000] = {0};

    if (pMsg == NULL)
    {
        printf(“TestFun: pMsgis NULL!\n”);
        return;
    }
    memcpy(buf, pMsg, sizeof(buf)-1);

    // 执行后续流程
}

总结
不管是开发什么样的软件产品,只要需要写代码,那么就离不开代码评审。通过代码评审,我们不仅可以很快发现代码中存在的问题,而且可以发现自己思维的缺陷及在技术上的欠缺,这对提升开发人员本身的素质也是很有好处的。

在代码评审的时候,大家要本着实事求是的态度,不要因为怕伤了和气而不好意思将问题讲出来,更不要由代码问题而转向对开发人员的人身攻击(例如说某人写的代码太烂了)。我们始终要记住大家的共同目标:将产品做好。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
4月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
138 1
|
4月前
|
存储 C语言
【C语言】基础刷题训练4(含全面分析和代码改进示例)
【C语言】基础刷题训练4(含全面分析和代码改进示例)
|
2月前
|
存储 搜索推荐 C语言
深入C语言指针,使代码更加灵活(二)
深入C语言指针,使代码更加灵活(二)
|
2月前
|
存储 程序员 编译器
深入C语言指针,使代码更加灵活(一)
深入C语言指针,使代码更加灵活(一)
|
2月前
|
C语言
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
|
3月前
|
安全 C语言
在C语言中,正确使用运算符能提升代码的可读性和效率
在C语言中,运算符的使用需要注意优先级、结合性、自增自减的形式、逻辑运算的短路特性、位运算的类型、条件运算的可读性、类型转换以及使用括号来明确运算顺序。掌握这些注意事项可以帮助编写出更安全和高效的代码。
56 4
|
2月前
|
C语言
C语言练习题代码
C语言练习题代码
|
3月前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
628 6
|
3月前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
215 5
|
3月前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
444 3