本文是对陈皓所写<编程修养>的学习笔记,原文请参考
编程修养主要将的是写程序的”修养”,其实就是写程序的习惯和规范等等.主要是作者总结C语言方面的一些注意点.
01、版权和版本
02、缩进、空格、换行、空行、对齐
03、程序注释
04、函数的[in][out]参数
05、对系统调用的返回进行判断
06、if 语句对出错的处理
07、头文件中的#ifndef
08、在堆上分配内存
09、变量的初始化
10、h和c文件的使用
11、出错信息的处理
12、常用函数和循环语句中的被计算量
13、函数名和变量名的命名
14、函数的传值和传指针
15、修改别人程序的修养
16、把相同或近乎相同的代码形成函数和宏
17、表达式中的括号
18、函数参数中的const
19、函数的参数个数
20、函数的返回类型,不要省略
21、goto语句的使用
22、宏的使用
23、static的使用
24、函数中的代码尺寸
25、typedef的使用
26、为常量声明宏
27、不要为宏定义加分号
28、||和&&的语句执行顺序
29、尽量用for而不是while做循环
30、请sizeof类型而不是变量
31、不要忽略Warning
32、书写Debug版和Release版的程序
1、 版权和版本
对于C/C++的文件,文件头应该有类似这样的注释:
/*************************************************************************
* 文件名:network.c
* 文件描述:网络通讯函数集
* 创建人: Hao Chen, 2003年2月3日
* 版本号:1.0
* 修改记录:
************************************************************************/
而对于函数来说,应该也有类似于这样的注释:
/*================================================================*
* 函 数 名:XXX*
* 参 数:
* type name [IN] : descripts
* 功能描述:
* ..............
* 返 回 值:成功TRUE,失败FALSE
* 抛出异常:
* 作 者:ChenHao 2003/4/2
================================================================*/
2、 缩进、空格、换行、空行、对齐
1) 缩进一般是一个TAB或则4个空格.用vim的时候,默认TAB代表8个空格,可以对vim进行设置.在/etc/vimrc中添加 set tabstop=4.
2) 空格:操作符间(运算符还有括号都算),函数调用间的各个参数都要加空格.
3) 换行:条件语句过长,函数参数过多的时候都可以换行,一行写一个
4) 空行:程序块间最好加空行.
5) 对齐:用Tab键对齐函数声明时候的类型,变量明,注释.
3、 程序注释
一般来说你需要至少写这些地方的注释:文件的注释、函数的注释、变量的注释、算法的注释、功能块的程序注释。主要就是记录你这段程序是干什么的?你的意图是什么?你这个变量是用来做什么的?等等。
注意的地方:
1) 由于某些版本编译器不支持行注释(“//”),所用尽量都用块注释(“/* */”).
2) 解决块注释不能嵌套,可以使用预编译.使用”#if 0”和”#endif”之间的代码将不被编译,等同于注释而且还可以嵌套.
4、 函数的[in][out]参数
写有参数的函数时,首要工作,就是要对传进来的所有参数进行合法性检查。而对于传出的参数也应该进行检查,这个动作当然应该在函数的外部,也就是说,调用完一个函数后,应该对其传出的值进行检查。
这种情况特别是在[in]为指针型的时候特别重要,如果不判读这个指针是否为空,有可能导致崩溃.通常比较好的方法是使用断言(assert).断言只要在debug版本的程序中有用,使用要包含一个assert.h头.使用方法大概是assert(需要判读的变量),如果assert的参数为假,责程式就会在此结束并跳出出错信息.
5、 对系统调用的返回进行判断
比如打开文件,socket,分配内存等等系统调用后,需要对返回的指针,地址,fd等进行一个判读.
6、 if 语句对出错的处理
如果if函数仅仅的作用是判读是否有错误,然后有一大段的语句是用于正常情况下.那么就不要用这样的结构:
if ( ch >= '0' && ch <= '9' ){
/* 正常处理代码 */
}else{
/* 输出错误信息 */
printf("error ......\n");
return ( FALSE );
}
而用以下的结构,不要用else,if仅仅用来判读错误情况.这样突出了错误的条件.
if ( ch < '0' || ch > '9' ){
/* 输出错误信息 */
printf("error ......\n");
return ( FALSE );
}
/* 正常处理代码 */
......
7、 头文件中的#ifndef
为了防止两个C文件都include一个头文件而产生的声明冲突的问题,可以将头文件的内容都放在放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:
#ifndef <标识>
#define <标识>
……
#endif
8、 在堆上分配内存
在”栈 stack”与”堆 heap”上内存分配是有区别的.在栈上通常是静态分配,如变量声明,声明数组等,系统会在函数返回时候自动释放分配的内存.在堆上是动态分配,通常由malloc,calloc,realloc调用,系统不会自动释放,需要调用free手动释放.如果忘记释放就会产生”内存泄露”(Memory Leak)问题.
栈内存分配
—————
char*
AllocStrFromStack()
{
char pstr[100];
return pstr;
}
堆内存分配
—————
char*
AllocStrFromHeap(int len)
{
char *pstr;
if ( len <= 0 ) return NULL;
return ( char* ) malloc( len );
}
对于malloc和free的操作有以下规则:
1) 配对使用,有一个malloc,就应该有一个free。(C++中对应为new和delete)
2) 尽量在同一层上使用,不要像上面那种,malloc在函数中,而free在函数外。最好在同一调用层上使用这两个函数。
3) malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。
9、 变量的初始化
C/C++编译器不会帮助你初始化变量.对下面三个情况需要初始化:
1) 对malloc分配的内存进行memset清零操作。(可以使用calloc分配一块全零的内存)
2) 对一些栈上分配的struct或数组进行初始化。(最好也是清零)
3) 而对于全局变量,和静态变量,一定要声明时就初始化。
但也并非所有的变量都要初始化,主要是涉及指针的一定要,下面是例子:
如:以下这种情况,则不需要。
char *pstr; /* 一个字符串 */
pstr = ( char* ) malloc( 50 );
if ( pstr == NULL ) exit(0);
strcpy( pstr, "Hello Wrold" );
但如果是下面一种情况,最好进行内存初始化。(指针是一个危险的东西,一定要初始化)
char **pstr; /* 一个字符串数组 */
pstr = ( char** ) malloc( 50 );
if ( pstr == NULL ) exit(0);
/* 让数组中的指针都指向NULL */
memset( pstr, 0, 50*sizeof(char*) );
10、 h和c文件的使用
一般来说,H文件中是declare(声明),C文件中是define(定义)。H文件中一般是变量、宏定义、枚举、结构和函数接口的声明,就像一个接口说明文件一样。而C文件则是实现细节。
此外需要初始化的全局变量不要放在H文件中,作者这样做的目的是为了减少生成文件的大小.如errmsg这个错误信息结构体如果在H文件中,那么每个C调用一次就要多一个副本.比较好的做法是将errmsg放在一个C中,其他C用的时候加上 extern的外部声明,这样其他C在应用的时候将去连接这个唯一的副本.
11、 出错信息的处理
应该统一管理错误信息或则是提示信息,而不只是在出错时候一个简单的printf而已.具体处理方法如下:
声明出错代码
#define err_xxx x
声明出错信息
char * errmsg[]={
/* x */ “xxxxx”
…
};
程序中先定义错误代码的全局变量 errno ,然后在定义一个显示错误代码和信息的函数 perror.
好了,如果你程序中出错就用什么的错误代码给errno赋值,如果要show错误信息就调用perror好了.对应的错误信息就是*errmsg[errno]
12、 常用函数和循环语句中的被计算量
循环体中不随着循环而变化的语句应该放在循环体外.
常被调用的函数中的不变的量也应该分配为static,这样就只会被开辟一次内存.
13、 函数名和变量名的命名
好的变量名或是函数名,有以下的规则:
1) 直观并且可以拼读,可望文知意,不必“解码”。
2) 名字的长度应该即要最短的长度,也要能最大限度的表达其含义。
3) 不要全部大写,也不要全部小写,应该大小写都有,如:GetLocalHostName 或是 UserAccount。
4) 可以简写,但简写得要让人明白,如:ErrorCode -> ErrCode, ServerListener -> ServLisner.
5) 为了避免全局函数和变量名字冲突,可以加上一些前缀,一般以模块简称做为前缀。
6) 全局变量统一加一个前缀或是后缀,让人一看到这个变量就知道是全局的。
7) 用匈牙利命名法命名函数参数,局部变量。但还是要坚持“望文生意”的原则。
8) 与标准库(如:STL)或开发库(如:MFC)的命名风格保持一致。
14、 函数的传值和传指针
就是说忘函数传递参数的时候,要分清楚到底是在传值还是指针,只有传递的是指针才会对实参进行修改.作者在这边举了一个列子,我一开始也半天没看懂.可能是作者在这边的解释有歧义吧,我的理解是这样的.
程序中试图通过函数GetVersion给指针ver分配空间,但这种方法根本没有什么作用,原因就是——这是传值,不是传指针。更好的更容易理解可以这么说:传进去的是个指针,但是在函数中没有对指针对象进行修改,而只是修改指针本省的值,自然实参就没有变化了.(指针真是个难题,没想到至少有三年程序经验的我又在这卡了一下,哈哈,基础没打好!准备下周好好重新学习一下指针!)
void
GetVersion(char* pStr)
{
pStr = malloc(10);
strcpy ( pStr, "2.0" );
}
main()
{
char* ver = NULL;
GetVersion ( ver );
...
free ( ver );
}
15、 修改别人程序的修养
修改别人的程序时,请不要删除别人的程序,如果你觉得别人的程序有所不妥,请注释掉,然后添加自己的处理程序.
16、 把相同或近乎相同的代码形成函数和宏
如果你有一些程序的代码片段很相似,或直接就是一样的,请把他们放在一个函数中。而如果这段代码不多,而且会被经常使用,你还想避免函数调用的开销,那么就把他写成宏吧。
而且这样做还可以做到”一改白改”…
17、 表达式中的括号
复杂的表达式最好用括号,即使你很清楚其中的优先级关系,这样做也可以让看你程序人很清晰.
18、 函数参数中的const
函数的指针参数如果是只读的,不需要修改的就加上const吧,这样别人就清楚这个变量的[in/out]性质.虽然在c中,const后的指针内容依然能够被修改,应为编译器会强制转化,但对有const的指针内容被修改编译器还是会报告一个warning的.
今天刚看C++这块,发现就与C不同了,C++对const的控制是比较严格的.没完整学过C++,不管感觉还是有空要好好看看的.现在真的发现自己基础知识是一塌糊涂的!
19、 函数的参数个数(多了请用结构)
函数的参数最好不要太多,6个就好了.如果实在需要,就把参数放在结构体中.
20、 函数的返回类型,不要省略
函数的返回类型最好都写上,不管是void还是int什么的.
关于void函数其中也要加return我的经验却说,如果加了编译器会报错or warning吧?不知道作者用的什么编译器?
21、 goto语句的使用
一般不得已就不使用goto就是了.本人就很鄙视喜欢用goto的,看其代码那个叫累!
22、 宏的使用
定义一个宏,就是在程序编译的时候,都会把遇到的宏替换成其定义的代码.可以形象的说为”展开”.但是就是因为其就是简单的展开,你不知道你调用的宏和他的参数在展开后是什么样子,作者给了一个很好的列子:
#define MAX(a, b) a>b?a:b
MAX( 17+32, 25+21 ) 展开后就是 17+32>25+21?17+32:25+21,很难看懂了吧!
所以宏在使用时,参数一定要加上括号,上述的那个例子改成如下所示就能解决问题了。
#define MAX( (a), (b) ) (a)>(b)?(a):(b).
所以使用宏还是要小心的好,除了加括号外,在写的时候遇到了也想象一下编译器展开的后的模样是什么?
23、 static的使用
static就上表示静态,可以用来定义变量和函数.其有两个特定:1.静态,2.有作用域.
作用域就是其是全局还是函数中的,那么仅仅在这个作用域中能使用这个量.
静态这样理解:在程序在作用域中第一次遇到static申明的东东,那么就给它开辟内存,并且以后遇见了也不会在开辟,离开作用域后内存的内容也不改变,这样就应该很好理解静态的概念了吧?
作者说static还有很好的访问控制作用,其实就是用了其作用域的特定. 在C中如果一个函数或是一个全局变量被声明为static,那么,这个函数和这个全局变量,将只能在这个C文件中被访问,如果别的C文件中调用这个C文件中的函数,或是使用其中的全局(用extern关键字),将会发生链接时错误。这个特性可以用于数据和程序保密。
24、 函数中的代码尺寸
一个函数中的代码最好不要超过600行.
函数的功能越单一越好.
25、 typedef的使用
typedef就是给类型名取别名,这个在需要移植的程序中就很好用了.不同平台的类型明不同,但是在预编译(头文件中)可以根据平台取一致的别名.
使用在结构体申明中,如
typedef struct _hostinfo {
HOSTID_T host;
INT32_T swap;
} HostInfo;
那么在定义变量时候就可以简洁的写成: HostInfo* phinfo;
使用在函数指针时:
typedef int (*RsrcReqHandler)(
void *info,
JobArray *jobs,
AllocInfo *allocInfo,
AllocList *allocList);
定义的时候这样应用:RsrcReqHandler * pRsrcHand;
26、 为常量声明宏
把程式中经常需要用的常量定义为宏,即可以增加程序的可读性,又可以在修改的时候省时省力.
27、 不要为宏定义加分号
如果理解宏的用法是”展开”,那么就可以很清晰知道为什么宏不要加分号了.
28、 ||和&&的语句执行顺序
条件语句中两个判读语句被这两个操作符连接的时候,其执行顺序是不一样的.
A || B: 那么只有A为假,那么B才执行.
A && B: 只有A为真的时候,B才执行.
所以在用于条件语句时候就要小心这种执行特性,并非所有的条件语句都会执行的.
29、 尽量用for而不是while做循环
因为for可以把循环的条件,循环的判读都可以一眼看清.
30、 请sizeof类型而不是变量
也就是说,如果有一变量为 int a[10],如果要算其长度,最好用 sizeof(int)*10,而不是sizeof(a);
而且如果变量是指针型的话,这样还特别容易出错.比如 int *p = malloc(10),那么sizeof(p)就只是指针的长度,而不是整个数组.
数组名就是数组的首地址,那么为什么sizeof可以用于数组而不能用于指针呢? 因为数组是记录整个数据长度的,而指针是没有这个特性的.比如a[20],就出错,而 *(p+20)就没有问题.
31、 不要忽略Warning
虽然warning不会导致程序编译的不成功,但是还是需要改进的.
一般来说,一面的一些警告信息是常见的:
1)声明了未使用的变量。(虽然编译器不会编译这种变量,但还是把它从源程序中注释或是删除吧)
2)使用了隐晦声明的函数。(也许这个函数在别的C文件中,编译时会出现这种警告,你应该这使用之前使用extern关键字声明这个函数)
3)没有转换一个指针。(例如malloc返回的指针是void的,你没有把之转成你实际类型而报警,还是手动的在之前明显的转换一下吧)
4)类型向下转换。(例如:float f = 2.0; 这种语句是会报警告的,编译会告诉你正试图把一个double转成float,你正在阉割一个变量,你真的要这样做吗?还是在2.0后面加个f吧,不然,2.0就是一个double,而不是float了)
32、 书写Debug版和Release版的程序
在写程序的时候,可以在预编译中添加debug机制,例如:
#ifdef DEBUG
void TRACE(char* fmt, ...)
{
......
}
#else
#define TRACE(char* fmt, ...)
#endif
在所有的程序中用TRACE来输出调试的信息. 在编译程序的时候如果添加DEBUG申明,即 cc –dDEBUG,那么就可以进入debug模式了.为什么这边还用到了宏定义了呢?因为如果不使用debug,那么程序中对应的TRACE语句都要为空(展开),这样不也能减小生成文件的大小.
顺便提一下,两个很有用的系统宏,一个是“__FILE__”,一个是“__LINE__”,分别表示,所在的源文件和行号.
综上所述32条,都是为了三大目的——
1、程序代码的易读性。
2、程序代码的可维护性,
3、程序代码的稳定可靠性。