【在主画面加入捷径】
       
【选择语系】
繁中 简中

[C 语言] 程序设计教学:如何藉由外部工具改善 C 程序代码的品质

【赞助商连结】

    藉由调整编译时的参数,编译器可以帮我们抓出 C 程序代码里的一些错误,但我们仍然可以藉由一些外部工具来改善 C 程序代码。本文列出一些检查 C 程序代码的要点,分为三个部分:

    • 使用一致的撰码风格
    • 进行静态程序代码检查
    • 进行内存用量检查

    本文所提及的软件对于编译 C 程序代码不是必需的,但可提升 C 程序代码的品质,故建议将这些软件加入自己的工具组 (toolbox) 中。

    使用一致的撰码风格 (Coding Style)

    撰码风格 (coding style) 或撰码标准 (coding standard) 是指在撰写程序代码时排比程序代码的方式。对于中大型项目,保持一致的撰码风格,阅读程序代码时比较赏心悦目,也比较容易理解一些。有些程序撰写者凭着直觉去写程序代码,不太注重撰码风格;有些程序撰写者对撰码风格有着宗教般的情怀。由于撰码风格并不是死记一套守则后马上就会写,而是在学习的过程中逐渐养成自己的习惯,所以我们在一开始时就提出这个概念。

    常见的 C 撰码风格有以下数种:

    注:K & R 风格是指出现在 The C Programming Language 一书中的撰码风格,但原着中并没有明列该风格的守则。

    这些撰码风格主要影响缩排 (indentation) 和括号的摆放方式。

    另外一个常见的主题是变量 (variable) 的撰码风格:

    • PascalCase (大驼峰命名法)
    • camelCase (小驼峰命名法)
    • snake_case

    一般来说,会全用 snake case 来命名变量,但若团队使用驼峰式命名法也无妨,编译器不会将命名风格视为错误。

    如果读者有机会到科技公司上班的话,公司内部可能也会有自己的撰码风格,撰写公司项目时请以公司内部的风格为标准;撰码风格的出发点是在同一份项目内保持一致,不必变成口水战的话题。

    TAB vs Space

    建议:不要调整 TAB 相对应的空白数,让 TAB 保持 8 个空白的寛度,除此之外,使用空白键 (space) 来排版。

    由于 C 语言没有限制空白的使用方式,程序撰写者可以依自己喜好来整理 C 程序代码。一般常见的缩进 (indentation) 方式有 2 个空白、4 个空白、8 个空白或一个 TAB;笔者目前仿 K & R 风格,用 4 个空白,读者可自己选用自己喜好的方式。TAB vs. space 也是早期的口水战来源之一,但现在有许多自动重排程序代码的软件,战这个其实义意不大。

    TAB vs. space 真正会出问题的地方在于撰写 Makefile 时。由于 Makefile 一定要用 TAB 来缩进,但 TAB 和空白键在视觉上无法区分,所以一般会建议在 C 程序代码中用 2 个空白或 4 个空白,而保留 TAB 为相当于 8 个空白的寛度,并仅用于 Makefile 中。

    自动程序代码重排

    虽然保持良好的撰码风格对项目有所帮助,但手动修正大量程序代码相当耗时、没效率,透过软件可以自动化一部分的工作。有些 IDE 内建自动重排的功能,可以省下手动排版的时间。或者可以用一些终端机工具,如下:

    由于 C 语言的撰码风格不只一种,这类程序代码重排工具有许多细微的选项可调整。如果愿意花一些时间,可以自行调整出一套风格设定档,以后就持续沿用下去。对于初学者来说,也有一些现成的「套餐」可以选择;像是 indent 软件支援 -gnu (GNU)、-kr (K & R)、-orig (BSD) 等风格的参数。台湾不少 C 程序书籍采用接近 K & R 的风格,初学者可以参考。

    以下 indent 指令可快速以 K & R 风格排列程序代码,将原有程序代码以新的风格取代:

    $ indent -kr file.c

    也可以用 astyle 达到同样的效果:

    $ astyle --style=kr file.c

    进行静态程序代码检查 (Static Code Analysis)

    除了修正撰码风格外,还可用静态程序分析工具 (linter) 扫描程序代码以检查一些不佳的写法,之后再手动修改。一些工具如下:

    以下是一个简短的不良例子:

    #include <stdio.h>
    
    int main()
    {
        if (1) {
            printf("true\n");
        } else {
            printf("false\n");
        }
    
        return 0;
    }

    splint 检查此程序代码:

    $ splint file.c
    Splint 3.1.2 --- 21 Sep 2017
    
    file.c: (in function main)
    file.c:5:9: Test expression for if not boolean, type int: 1
      Test expression type is not boolean or int. (Use -predboolint to inhibit
      warning)
    
    Finished checking --- 1 code warning

    但是使用 cppcheck 检查这段程序则认定这段程序代码没有问题。不同的程序代码检查器会有不同的判定标准,程序人还是要自己做最后的判断。程序代码检查器可协助我们检查出不是错误但相对不良的写法,对于改良程序代码品质会有所帮助。

    附带一提,Splint 目前的发展停滞,如果碰到一些臭虫,原开发团队可能也不会再修了 (可见这里)。建议可转用其他的程序代码检查器。

    进行内存用量检查 (Memory Usage Detection)

    由于 C (或 C++) 需手动管理内存,即使程序执行正确,也有可能会出现内存泄漏 (memory leak)。程序开始用到 mallocfree 等相关函式时最好就加入内存使用检查。对于初期写的小型程序来说,即使忘了释放内存,在程序结束后操作系统仍会回收内存,但最好还是在初期就养成良好的习惯。

    在 GNU/Linux 上最知名的内存检查软件是 Valgrind,使用方式相当简单:

    $ valgrind ./file
    ==188371== Memcheck, a memory error detector
    ==188371== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
    ==188371== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
    ==188371== Command: ./file
    ==188371== 
    ==188371== 
    ==188371== HEAP SUMMARY:
    ==188371==     in use at exit: 0 bytes in 0 blocks
    ==188371==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
    ==188371== 
    ==188371== All heap blocks were freed -- no leaks are possible
    ==188371== 
    ==188371== For counts of detected and suppressed errors, rerun with: -v
    ==188371== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

    这个例子没有内存泄露。我们来看一个有内存泄露的例子:

    $ valgrind ./file
    ==188401== Memcheck, a memory error detector
    ==188401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
    ==188401== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
    ==188401== Command: ./file
    ==188401== 
    ==188401== 
    ==188401== HEAP SUMMARY:
    ==188401==     in use at exit: 0 bytes in 1 blocks
    ==188401==   total heap usage: 1 allocs, 0 frees, 0 bytes allocated
    ==188401== 
    ==188401== LEAK SUMMARY:
    ==188401==    definitely lost: 0 bytes in 1 blocks
    ==188401==    indirectly lost: 0 bytes in 0 blocks
    ==188401==      possibly lost: 0 bytes in 0 blocks
    ==188401==    still reachable: 0 bytes in 0 blocks
    ==188401==         suppressed: 0 bytes in 0 blocks
    ==188401== Rerun with --leak-check=full to see details of leaked memory
    ==188401== 
    ==188401== For counts of detected and suppressed errors, rerun with: -v
    ==188401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

    Valgrind 是检查整个程序,找出那个部分有内存泄露是程序设计者的责任。

    目前 Valgrind 可在 GNU/Linux、Solaris、OS X (10.12 以前) 上执行,若读者使用其他系统则需自行寻找替代方案,像是在 Windows 或 GNU/Linux 可用 Dr. Memory 来检查内存使用的情形。

    C 本身没有内建的垃圾回收器 (garbage collector),标准函式库也没有,如果需要时倒是有一些第三方方案,像是这个项目。学习 C 语言时,操作指针和管理内存也是学习的一环,最好不要一开始时就太依赖垃圾回收器。