本文发自 http://www.binss.me/blog/the-excerpt-and-review-of-Programming-Training/,转载请注明出处。

本文是本人在阅读陈皓大大的博客《编程修养》系列文章后的一些摘录和心得。原文地址:

http://blog.csdn.net/haoel/article/details/2872

陈皓认为,好的程序员应该有以下几方面的素质:

  1. 有专研精神,勤学善问. 举一反三。

  2. 积极向上的态度,有创造性思维。

  3. 与人积极交流沟通的能力,有团队精神。

  4. 谦虚谨慎,戒骄戒燥。

  5. 写出的代码质量高。包括:代码的稳定. 易读. 规范. 易维护. 专业。

要了解一个程序员,首先就是看他写的代码,因为程序代码可以看出一个程序员的素质和修养。程序就像一个作品,有素质有修养的程序员的作品必然是一图精美的图画,一首美妙的歌曲,一本赏心悦目的小说。

那么如何写出有”修养“的代码呢?他总结了32点,在阅读的同时,加上了个人的体会。

  1. 版权和版本

    好的程序员会给自己的每个函数,每个文件,都注上版权和版本。这样的描述可以让人对一个函数,一个文件有一个总体的认识,对代码的易读性和易维护性有很大的好处。

    binss:对于Sublime,可以通过插件的方式快速插入注释,如:

    /***********************************************************
     * @FileName:      untitled
     * @Author:        binss
     * @Create:        2015-08-29 10:34:28
     * @Description:   untitled
     * @History:       <time>  <version>  <desc>
    ***********************************************************/
  2. 缩进. 空格. 换行. 空行. 对齐

    好看的代码会让人的心情愉快,读起代码也就不累,工整. 整洁的程序代码,通常更让人欢迎,也更让人称道。

    缩进:使用一个TAB键或是4个空格进行缩进。

    binss:良好的缩进是代码是否易读的基础。我的做法是用4个空格进行缩进,因为在写Python的时候,TAB和空格混用是致命的错误,而PEP8规范规定,不允许使用TAB。为了愉快地用TAB,我开启了Sublime中的translate_tabs_to_spaces,它会自动将TAB转为4个空格。

    空格:有效的利用空格可以让你的程序读进来更加赏心悦目。一般来说,语句中要在各个操作符间加空格,函数调用时,要以各个参数间加空格。如:

    ha = ( ha * 128 + *key++ ) % tabPtr->size;
    if ( ( hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) ) == NULL )
    {
    }

    换行:函数参数多的时候. 条件语句条件多的时候也要写成换行。

    binss:超过80个字符坚决换行。这个习惯是当年写COBOL养成的。。。

    空行:程序块间,最好加上空行。

    对齐:用TAB键对齐你的一些变量的声明或注释,会让你的程序好看一些。

    binss:然而这样做的话,语法检查器老是提示缩进不合法。。。

  3. 程序注释

    养成写程序注释的习惯,这是每个程序员所必须要做的工作。

    binss:对于那些自己用的代码,比如博客之类的,都非常懒地没有加注释。结果在隔了半年后进行博客2.0的开发时发现看不懂以前的代码了...从此以后就老老实实地加一些注释了。

  4. 函数的[in][out]参数

    对于传入参数,应该先判断一下传进来的那个指针是不是为空。如果传进来的指针为空的话,一个大的系统就会因为这一个小的函数而崩溃。一种更好的技术是使用断言(assert)。

  5. 对系统调用的返回进行判断

    有时候,函数的返回值并不会像你期待的那样。比如在fopen的时候出错会返回一个空指针,如果你不对其返回结果进行判断,那么你就会一直郁闷为什么直接写不进文件。

  6. if 语句对出错的处理

    如果if中“正常处理代码”很长时,最好不要用else。先判断错误,如:

    if ( ch < '0' || ch > '9' ){
        /* 输出错误信息 */
        printf("error ....../n");
        return ( FALSE );
    }
    /* 正常处理代码 */

    这样的结构突出了错误的条件,让别人在使用你的函数的时候,第一眼就能看到不合法的条件,于是就会更下意识的避免。

  7. 头文件中的#ifndef

    为了避免重复包含头文件而导致声明冲突,头文件应该使用:

    #ifndef  <标识>
    #define <标识>
    ......
    #endif

    <标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h的标识为_STDIO_H_。

  8. 在堆上分配内存

    由malloc/new函数分配的内存就是从堆上分配内存。从堆上分配的内存一定要自己释放。用free/

    delete释放,不然就内存泄露了。由于堆内存不会自动释放,于是,系统的可分配内存会越来越少,直到系统崩溃。

    对于malloc/new和free/delete的操作有以下规则:

    • 配对使用,有一个malloc/new,就应该有一个free/delete。

    • 尽量在同一层上使用,不要malloc在函数中,而free在函数外。最好在同一调用层上使用这两个函数。

    • malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。

  9. 变量的初始化

    变量一定要被初始化再使用。如果使用了没有初始化的变量,结果未知。

    • 对malloc分配的内存进行memset清零操作。(可以使用calloc分配一块全零的内存)

      binss:C++的new,对于有构造函数的类,不论有没有括号,都用构造函数进行初始化;如果没有构造函数,则不加括号的new只分配内存空间,不进行内存的初始化,而加了括号的new会在分配内存的同时初始化为0。

    • 对一些栈上分配的struct或数组进行初始化。(最好也是清零)

    • 对于全局变量,和静态变量,一定要声明时就初始化。因为你不知道它第一次会在哪里被使用。所以使用前初始这些变量是比较不现实的,一定要在声明时就初始化它们。

  10. h和c文件的使用

    一般来说,h文件中是declare(声明),c文件中是define(定义)。不要把定义写到声明中。

    binss:实际应用中,对于只有一行的函数,常常会被写到h文件中,如:

    int getMoney() { return m_iMoney; };

    这样做的原因是在h中可以一行写完,而不必到c中写多三行。

    到底这样做好不好,欢迎指正。

  11. 出错信息的处理

    出错信息或是提示信息,应该统一处理,而不是硬编码在代码中。

    如何统一处理?

    #define     ERR_NO_ERROR    0  /* No error                 */
    #define     ERR_OPEN_FILE   1  /* Open file error          */
    ......

    然后

    char* errmsg[] = {
            /* 0 */       "No error",
            /* 1 */       "Open file error",
            ......
    };

    在出错时,使用errmsg[ERR_NO_ERROR]

    这样做有利同种错误出一样的信息,统一用户界面。

  12. 常用函数和循环语句中的被计算量

    对于循环函数的被计算量,如果不变,则没有必要重复计算,而是放到循环体外。对于常用函数中的不变量,设为static以省去分配内存的开销。

  13. 函数名和变量名的命名

    • 直观并且可以拼读,可望文知意,不必“解码”。

    • 名字的长度应该即要最短的长度,也要能最大限度的表达其含义。

    • 不要全部大写,也不要全部小写,应该大小写都有,如:GetLocalHostName 或是 UserAccount。

    • 可以简写,但简写得要让人明白,如:ErrorCode -> ErrCode, ServerListener -> ServLisner,UserAccount -> UsrAcct 等。

    • 为了避免全局函数和变量名字冲突,可以加上一些前缀,一般以模块简称做为前缀。

    • 全局变量统一加一个前缀或是后缀,让人一看到这个变量就知道是全局的。

    • 用匈牙利命名法命名函数参数,局部变量。但还是要坚持“望文生意”的原则。

    • 与标准库(如:STL)或开发库(如:MFC)的命名风格保持一致。

    binss:项目中全都是匈牙利命名法...

  14. 函数的传值和传指针

    向函数传参数时,一般而言,传入非const的指针时,就表示,在函数中要修改这个指针把指内存中的数据。如果是传值,那么无论在函数内部怎么修改这个值,也影响不到传过来的值,因为传值是只内存拷贝。

    void GetVersion(char* pStr)
    {
        pStr = malloc(10);
        strcpy ( pStr, "2.0" );
    }
    
    main()
    {
        char* ver = NULL;
        GetVersion ( ver );
        cout<<ver;
        free ( ver );
    }

    binss:作者举了这个灰常好的例子。这个程序企图用GetVersion为ver分配空间并赋值,但是实际上并没有达到预期。因为修改的是指针的指向,而对于指针来说,它自身是作为值被传入函数中的,所以函数中的修改的只是指针的拷贝。那么什么是传指针呢?如下所示:

    void GetVersion(char** pStr)
    {
        *pStr = (char*)malloc(10);
        strcpy ( *pStr, "2.0" );
    }
    
    int main()
    {
        char* ver = NULL;
        GetVersion ( &ver );
        cout<<ver;
        free ( ver );
    }
  15. 修改别人程序的修养

    当你维护别人的程序时,请不要非常主观臆断的把已有的程序删除或是修改。如果你觉得别人的程序有所不妥,注释掉,然后添加自己的处理程序。这样. 可以让再维护的人很容易知道以前的代码更改的动作和意图,而且这也是对原作者的一种尊敬。

  16. 把相同或近乎相同的代码形成函数和宏

    如果你有一些程序的代码片段很相似,或直接就是一样的,把他们放在一个函数中。而如果这段代码不多,而且会被经常使用,你还想避免函数调用的开销,那么就把他写成宏。这样做的目的是为了避免修改时要改好几处地方,给维护带来巨大的麻烦

  17. 表达式中的括号

    如果一个比较复杂的表达式中,即使是你很清楚各个操作的优先级,也请加上括号

  18. 函数参数中的const

    对于一些函数中的指针参数,如果在函数中只读,请将其用const修饰,这样,别人一读到你的函数接口时,就会知道你的意图是这个参数是[in],如果没有const时,参数表示[in/out],注意函数接口中的const使用,利于程序的维护和避免犯一些错误。

  19. 函数的参数个数

    函数的参数个数最好不要太多,一般来说6个左右就可以了,众多的函数参数会让读代码的人一眼看上去就很头昏,而且也不利于维护。如果参数众多,还请使用结构来传递参数。这样做有利于数据的封装和程序的简洁性。这样做的好处是函数被修改时如果需要给函数增加参数,不需要更改函数接口,只需更改结构体和函数内部处理,而对于调用函数的程序来说,这个动作是透明的。

  20. 函数的返回类型,不要省略

    如果一个函数没有返回值,也请在函数前面加上void的修饰。而有的程序员偷懒,在返回int的函数则什么不修饰(因为如果不修饰,则默认返回int),这种习惯很不好,还是为了原代码的易读性,加上int吧。

    binss:C++已经强制要求返回类型了。这里想说的是尽量不要以void类型作为函数返回值,而是用int替代。当函数执行出错时,返回非0的错误码,有助于函数的调用者判断函数执行的结果。

  21. goto语句的使用

    强烈建议不要使用goto语句。

  22. 宏的使用

    宏只是一种定义,他定义了一个语句块,当程序编译时,编译器首先要执行一个“替换”源程序的动作,把宏引用的地方替换成宏定义的语句块,就像文本文件替换一样。这个动作叫“宏的展开”。

    宏在使用时,为了避免文本替换后计算优先级发生变化,参数一定要加上括号。

    所以,在宏的使用上还是要谨慎考虑,因为宏展开是的结果是很难让人预料的。而且虽然宏的执行很快(因为没有函数调用的开销),但宏会让源代码澎涨,使目标文件尺寸变大,,相反反而不能让程序执行得更快(因为执行文件变大,运行时系统换页频繁)。

    binss:C++建议用inline代替。它将代码放到符号表中,在编译时展开,没有调用开销但能够进行类型检查. 语法判断等功能。

  23. static的使用

    static的变量是有作用域的全局变量。如果一个函数或是一个全局变量被声明为static,那么,这个函数和全局变量将只能在这个C文件中被访问,如果别的C文件中调用这个C文件中的函数,或是使用其中的全局(用extern关键字),将会发生链接时错误。这个特性可以用于数据和程序保密。

    binss:extern和static不能一起用。因为extern修饰全局变量和函数,被修饰的变量和函数可以在别的文件里使用。而static修饰的变量和函数作用范围仅限于定义它的文件内部。

  24. 函数中的代码尺寸

    一个函数中的代码最好不要超过600行左右,越少越好。函数一般是完成一个特定的功能,千万忌讳在一个函数中做许多件不同的事。函数的功能越单一越好,一方面有利于函数的易读性,另一方面更有利于代码的维护和重用,功能越单一表示这个函数就越可能给更多的程序提供服务,也就是说共性就越多。

  25. typedef的使用

    用typedef给类型起别名,加强代码的易读性。

    binss:对于以下这种“原来名字可以那么长”的类型,使用typedef可以减少精神污染。

    before:

    boost::coroutines::asymmetric_coroutine<std::string>::push_type a;

    after:

    typedef boost::coroutines::asymmetric_coroutine<std::string>::push_type push_type;
    push_type a;

    before:

    void (*signal(int signum,void(* handler)(int)))(int);

    after:

    typedef void (*sig_t)( int );
    sig_t signal(int signum, sig_t handler);
  26. 为常量声明宏

    最好不要在程序中出现数字式的“硬编码”,如:

    int user[120];
    for ( i=0; i<120; i++)
    {
        ....
    }

    120是什么?为什么会是120?这种“硬编码”不仅让程序很难读,而且让程序很不好维护,如果要改变这个数字,得同时对程序中所有出现的地方进行更改。所以还是把常量声明成宏,这样,一改百改。如:

    #define MAX_USR_CNT 120

    有的程序员喜欢把这种变量声明为全局变量,其实,全局变量应该尽量地少用,因为它不利于封装,也不利于维护,而且对程序执行空间有一定的开销,一不小心就引起系统换页,造成程序执行速度效率等问题。所以声明成宏,即可以免去全局变量的开销,也会有速度上的优势。

  27. 不要为宏定义加分号

    宏属于编译预处理,不属于c语言语句,所以也不要在行尾加分号,而是在使用的时候才加分号。

  28. ||和&&的语句执行顺序

    它并不是你所想像的所有的表达式都会去执行:

    express1 || express2

    先执行表达式express1,如果为真,express2将不被执行

    express1 && express2

    先执行表达式express1,如果为假,express2将不被执行

    binss:这就是短路逻辑。在某些情况下避免了无谓的计算,也是好东西。

  29. 尽量用for而不是while做循环

    在for中,循环的初始. 结束条件. 循环的推进,都在一起,一眼看上去就知道这是一个什么样的循环,易于阅读和维护。

  30. 请sizeof类型而不是变量

    我们常常会用sizeof去求数组的大小,而求的是数组指针的大小。

    binss:这点不服。sizeof(array) / sizeof(array[0]) 的用法在很多地方都能看到。但是在分配内存时,用100 * sizeof(int) 来代替400 是个很好的习惯。

  31. 不要忽略Warning

    对于一些编译时的警告信息,请不要忽视它们。虽然,这些Warning不会妨碍目标代码的生成,但这并不意味着你的程序就是好的。必竟,并不是编译成功的程序才是正确的,编译成功只是万里长征的第一步,后面还有大风大浪在等着你。从编译程序开始,不但要改正每个error,还要修正每个warning。这是一个有修养的程序员该做的事。

  32. 书写Debug版和Release版的程序

    利用预编译技术,加入#ifdef DEBUG,调试的时候在编译选项加上-DDEBUG即可。不加DEBUG编译出的就是Release版。

这里再加上几条个人总结的素养

  1. 比较时常数放在左边,比如:

    NULL == X

    这样做的目的是避免把==写成=而发现不了

  2. 把同语义的代码块括起来,如:

    {
        ......
    }

    这样做可以使代码更容易理解。