Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2161312
  • 博文数量: 361
  • 博客积分: 10828
  • 博客等级: 上将
  • 技术积分: 4161
  • 用 户 组: 普通用户
  • 注册时间: 2010-01-20 14:34
文章分类

全部博文(361)

文章存档

2011年(132)

2010年(229)

分类: LINUX

2010-01-25 13:47:25

本文是对陈皓所写<编程修养>的学习笔记,原文请参考
 
    编程修养主要将的是写程序的”修养”,其实就是写程序的习惯和规范等等.主要是作者总结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、程序代码的稳定可靠性。
 
阅读(1228) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2010-01-28 15:57:32

很好的。。受教了。