本文发自 http://www.binss.me/blog/the-excerpt-and-review-of-Programming-Training/,转载请注明出处。
本文是本人在阅读陈皓大大的博客《编程修养》系列文章后的一些摘录和心得。原文地址:
http://blog.csdn.net/haoel/article/details/2872
陈皓认为,好的程序员应该有以下几方面的素质:
-
有专研精神,勤学善问. 举一反三。
-
积极向上的态度,有创造性思维。
-
与人积极交流沟通的能力,有团队精神。
-
谦虚谨慎,戒骄戒燥。
-
写出的代码质量高。包括:代码的稳定. 易读. 规范. 易维护. 专业。
要了解一个程序员,首先就是看他写的代码,因为程序代码可以看出一个程序员的素质和修养。程序就像一个作品,有素质有修养的程序员的作品必然是一图精美的图画,一首美妙的歌曲,一本赏心悦目的小说。
那么如何写出有”修养“的代码呢?他总结了32点,在阅读的同时,加上了个人的体会。
-
版权和版本
好的程序员会给自己的每个函数,每个文件,都注上版权和版本。这样的描述可以让人对一个函数,一个文件有一个总体的认识,对代码的易读性和易维护性有很大的好处。
binss:对于Sublime,可以通过插件的方式快速插入注释,如:
/*********************************************************** * @FileName: untitled * @Author: binss * @Create: 2015-08-29 10:34:28 * @Description: untitled * @History: <time> <version> <desc> ***********************************************************/
-
缩进. 空格. 换行. 空行. 对齐
好看的代码会让人的心情愉快,读起代码也就不累,工整. 整洁的程序代码,通常更让人欢迎,也更让人称道。
缩进:使用一个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:然而这样做的话,语法检查器老是提示缩进不合法。。。
-
程序注释
养成写程序注释的习惯,这是每个程序员所必须要做的工作。
binss:对于那些自己用的代码,比如博客之类的,都非常懒地没有加注释。结果在隔了半年后进行博客2.0的开发时发现看不懂以前的代码了...从此以后就老老实实地加一些注释了。
-
函数的[in][out]参数
对于传入参数,应该先判断一下传进来的那个指针是不是为空。如果传进来的指针为空的话,一个大的系统就会因为这一个小的函数而崩溃。一种更好的技术是使用断言(assert)。
-
对系统调用的返回进行判断
有时候,函数的返回值并不会像你期待的那样。比如在fopen的时候出错会返回一个空指针,如果你不对其返回结果进行判断,那么你就会一直郁闷为什么直接写不进文件。
-
if 语句对出错的处理
如果if中“正常处理代码”很长时,最好不要用else。先判断错误,如:
if ( ch < '0' || ch > '9' ){ /* 输出错误信息 */ printf("error ....../n"); return ( FALSE ); } /* 正常处理代码 */
这样的结构突出了错误的条件,让别人在使用你的函数的时候,第一眼就能看到不合法的条件,于是就会更下意识的避免。
-
头文件中的#ifndef
为了避免重复包含头文件而导致声明冲突,头文件应该使用:
#ifndef <标识> #define <标识> ...... #endif
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h的标识为_STDIO_H_。
-
在堆上分配内存
由malloc/new函数分配的内存就是从堆上分配内存。从堆上分配的内存一定要自己释放。用free/
delete释放,不然就内存泄露了。由于堆内存不会自动释放,于是,系统的可分配内存会越来越少,直到系统崩溃。
对于malloc/new和free/delete的操作有以下规则:
-
配对使用,有一个malloc/new,就应该有一个free/delete。
-
尽量在同一层上使用,不要malloc在函数中,而free在函数外。最好在同一调用层上使用这两个函数。
-
malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。
-
-
变量的初始化
变量一定要被初始化再使用。如果使用了没有初始化的变量,结果未知。
-
对malloc分配的内存进行memset清零操作。(可以使用calloc分配一块全零的内存)
binss:C++的new,对于有构造函数的类,不论有没有括号,都用构造函数进行初始化;如果没有构造函数,则不加括号的new只分配内存空间,不进行内存的初始化,而加了括号的new会在分配内存的同时初始化为0。
-
对一些栈上分配的struct或数组进行初始化。(最好也是清零)
-
对于全局变量,和静态变量,一定要声明时就初始化。因为你不知道它第一次会在哪里被使用。所以使用前初始这些变量是比较不现实的,一定要在声明时就初始化它们。
-
-
h和c文件的使用
一般来说,h文件中是declare(声明),c文件中是define(定义)。不要把定义写到声明中。
binss:实际应用中,对于只有一行的函数,常常会被写到h文件中,如:
int getMoney() { return m_iMoney; };
这样做的原因是在h中可以一行写完,而不必到c中写多三行。
到底这样做好不好,欢迎指正。
-
出错信息的处理
出错信息或是提示信息,应该统一处理,而不是硬编码在代码中。
如何统一处理?
#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]
。这样做有利同种错误出一样的信息,统一用户界面。
-
常用函数和循环语句中的被计算量
对于循环函数的被计算量,如果不变,则没有必要重复计算,而是放到循环体外。对于常用函数中的不变量,设为static以省去分配内存的开销。
-
函数名和变量名的命名
-
直观并且可以拼读,可望文知意,不必“解码”。
-
名字的长度应该即要最短的长度,也要能最大限度的表达其含义。
-
不要全部大写,也不要全部小写,应该大小写都有,如:GetLocalHostName 或是 UserAccount。
-
可以简写,但简写得要让人明白,如:ErrorCode -> ErrCode, ServerListener -> ServLisner,UserAccount -> UsrAcct 等。
-
为了避免全局函数和变量名字冲突,可以加上一些前缀,一般以模块简称做为前缀。
-
全局变量统一加一个前缀或是后缀,让人一看到这个变量就知道是全局的。
-
用匈牙利命名法命名函数参数,局部变量。但还是要坚持“望文生意”的原则。
-
与标准库(如:STL)或开发库(如:MFC)的命名风格保持一致。
binss:项目中全都是匈牙利命名法...
-
-
函数的传值和传指针
向函数传参数时,一般而言,传入非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 ); }
-
修改别人程序的修养
当你维护别人的程序时,请不要非常主观臆断的把已有的程序删除或是修改。如果你觉得别人的程序有所不妥,注释掉,然后添加自己的处理程序。这样. 可以让再维护的人很容易知道以前的代码更改的动作和意图,而且这也是对原作者的一种尊敬。
-
把相同或近乎相同的代码形成函数和宏
如果你有一些程序的代码片段很相似,或直接就是一样的,把他们放在一个函数中。而如果这段代码不多,而且会被经常使用,你还想避免函数调用的开销,那么就把他写成宏。这样做的目的是为了避免修改时要改好几处地方,给维护带来巨大的麻烦
-
表达式中的括号
如果一个比较复杂的表达式中,即使是你很清楚各个操作的优先级,也请加上括号
-
函数参数中的const
对于一些函数中的指针参数,如果在函数中只读,请将其用const修饰,这样,别人一读到你的函数接口时,就会知道你的意图是这个参数是[in],如果没有const时,参数表示[in/out],注意函数接口中的const使用,利于程序的维护和避免犯一些错误。
-
函数的参数个数
函数的参数个数最好不要太多,一般来说6个左右就可以了,众多的函数参数会让读代码的人一眼看上去就很头昏,而且也不利于维护。如果参数众多,还请使用结构来传递参数。这样做有利于数据的封装和程序的简洁性。这样做的好处是函数被修改时如果需要给函数增加参数,不需要更改函数接口,只需更改结构体和函数内部处理,而对于调用函数的程序来说,这个动作是透明的。
-
函数的返回类型,不要省略
如果一个函数没有返回值,也请在函数前面加上void的修饰。而有的程序员偷懒,在返回int的函数则什么不修饰(因为如果不修饰,则默认返回int),这种习惯很不好,还是为了原代码的易读性,加上int吧。
binss:C++已经强制要求返回类型了。这里想说的是尽量不要以void类型作为函数返回值,而是用int替代。当函数执行出错时,返回非0的错误码,有助于函数的调用者判断函数执行的结果。
-
goto语句的使用
强烈建议不要使用goto语句。
-
宏的使用
宏只是一种定义,他定义了一个语句块,当程序编译时,编译器首先要执行一个“替换”源程序的动作,把宏引用的地方替换成宏定义的语句块,就像文本文件替换一样。这个动作叫“宏的展开”。
宏在使用时,为了避免文本替换后计算优先级发生变化,参数一定要加上括号。
所以,在宏的使用上还是要谨慎考虑,因为宏展开是的结果是很难让人预料的。而且虽然宏的执行很快(因为没有函数调用的开销),但宏会让源代码澎涨,使目标文件尺寸变大,,相反反而不能让程序执行得更快(因为执行文件变大,运行时系统换页频繁)。
binss:C++建议用inline代替。它将代码放到符号表中,在编译时展开,没有调用开销但能够进行类型检查. 语法判断等功能。
-
static的使用
static的变量是有作用域的全局变量。如果一个函数或是一个全局变量被声明为static,那么,这个函数和全局变量将只能在这个C文件中被访问,如果别的C文件中调用这个C文件中的函数,或是使用其中的全局(用extern关键字),将会发生链接时错误。这个特性可以用于数据和程序保密。
binss:extern和static不能一起用。因为extern修饰全局变量和函数,被修饰的变量和函数可以在别的文件里使用。而static修饰的变量和函数作用范围仅限于定义它的文件内部。
-
函数中的代码尺寸
一个函数中的代码最好不要超过600行左右,越少越好。函数一般是完成一个特定的功能,千万忌讳在一个函数中做许多件不同的事。函数的功能越单一越好,一方面有利于函数的易读性,另一方面更有利于代码的维护和重用,功能越单一表示这个函数就越可能给更多的程序提供服务,也就是说共性就越多。
-
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);
-
为常量声明宏
最好不要在程序中出现数字式的“硬编码”,如:
int user[120]; for ( i=0; i<120; i++) { .... }
120是什么?为什么会是120?这种“硬编码”不仅让程序很难读,而且让程序很不好维护,如果要改变这个数字,得同时对程序中所有出现的地方进行更改。所以还是把常量声明成宏,这样,一改百改。如:
#define MAX_USR_CNT 120
有的程序员喜欢把这种变量声明为全局变量,其实,全局变量应该尽量地少用,因为它不利于封装,也不利于维护,而且对程序执行空间有一定的开销,一不小心就引起系统换页,造成程序执行速度效率等问题。所以声明成宏,即可以免去全局变量的开销,也会有速度上的优势。
-
不要为宏定义加分号
宏属于编译预处理,不属于c语言语句,所以也不要在行尾加分号,而是在使用的时候才加分号。
-
||和&&的语句执行顺序
它并不是你所想像的所有的表达式都会去执行:
express1 || express2
先执行表达式express1,如果为真,express2将不被执行
express1 && express2
先执行表达式express1,如果为假,express2将不被执行
binss:这就是短路逻辑。在某些情况下避免了无谓的计算,也是好东西。
-
尽量用for而不是while做循环
在for中,循环的初始. 结束条件. 循环的推进,都在一起,一眼看上去就知道这是一个什么样的循环,易于阅读和维护。
-
请sizeof类型而不是变量
我们常常会用sizeof去求数组的大小,而求的是数组指针的大小。
binss:这点不服。sizeof(array) / sizeof(array[0]) 的用法在很多地方都能看到。但是在分配内存时,用100 * sizeof(int) 来代替400 是个很好的习惯。
-
不要忽略Warning
对于一些编译时的警告信息,请不要忽视它们。虽然,这些Warning不会妨碍目标代码的生成,但这并不意味着你的程序就是好的。必竟,并不是编译成功的程序才是正确的,编译成功只是万里长征的第一步,后面还有大风大浪在等着你。从编译程序开始,不但要改正每个error,还要修正每个warning。这是一个有修养的程序员该做的事。
-
书写Debug版和Release版的程序
利用预编译技术,加入#ifdef DEBUG,调试的时候在编译选项加上-DDEBUG即可。不加DEBUG编译出的就是Release版。
这里再加上几条个人总结的素养
-
比较时常数放在左边,比如:
NULL == X
这样做的目的是避免把==写成=而发现不了
-
把同语义的代码块括起来,如:
{ ...... }
这样做可以使代码更容易理解。