先来看一段伪代码:
/*一个下载器的简单伪代码*/
void downloader(){
/*创建一个套接字*/
int fd = socket(...);
/*填写sockaddr结构体地址*/
sturct sockaddr addr;
......
connect(fd,...);
/*发送下载请求*/
send(fd,...);
/*开始下载*/
recv(fd,...);
......
}
这段伪代码在一个函数里面把下载的所有事情都做了,假设没有这些注释,要别人仅仅靠这段代码去明白作者想干什么的确得花上点时间,易读性可维护性大大降低,但是如果我们这样子重新组织一下这段代码的话:
int create_socket(){
return socket(...);
}
int open_file(){
return open(...);
}
int connect_to_server(){
/*填写sockaddr结构体地址*/
sturct sockaddr addr;
......
return connect(fd,...);
}
int send_download_request(int socket){
/*构建下载请求*/
....
return send(socket,...);
}
start_download(int socket,int file_fd){
char buf[BUF_SIZE];
for(;;){
recv(socket,&buf,...);
write(file_fd,&buf,...);
}
}
void downloader(){
int socket = create_socket();
int file_fd = open_file();
int ret = connect_to_server(socket,...);
send_download_request(socket,...);
start_download(socket,file_fd...);
}
这样改写了源程序,从整体来看代码量没增加多少,但是注释减少了,程序的可读性也提高了,很多地方虽然没有注释,但是逻辑也清晰,比原来的更好了解了.
如果可以合理地构建子程序,不仅仅可以减少注释量,提高代码可读性,而且还可以减少错误的发生哦!
在代码大全里面,给出了以下这么几个原因解释为什么要建立子程序:
1.避免代码重复,若两段子程序内编写相似的代码,即意味着代码分解出现了错误,在这种情况应该把子程序重复的代码抽出,将重复的代码放入另外一个子程序中
其实这很好理解,一段多次需要调用的代码,最好的方法就是写一个子程序把这段代码包含进去,然后下次的时候就可以直接调用它而不需要再去编写了,同样,如果你要改错的话,只需要改一次而不需要整篇代码去找.
PS.一个程序员”正确的”懒的方法:当你很讨厌做一件可重复的事的时候,写个代码帮你完成这件令人厌恶的事,然后以后就可以愉快地”偷懒”了!
2.隐藏顺序:如果两个操作有相互依存的顺序关系,那么应该将两个操作放入一个子程序中,这样在调用的时候就可以避免一个操作依赖另外一个操作的问题
譬如刚才下载器的伪代码中:
int connect_to_server(){
/*填写sockaddr结构体地址*/
sturct sockaddr addr;
......
return connect(fd,...);
}
填写connect的目标地址这个操作必须在connect之前完成,不然就会connect失败,所以这种有先后顺序的代码可以直接放在一个子函数里面,从而避免出现顺序不对而导致的错误.
3.可以实现自我注解,子函数的名字取得适当,可以使得人从函数名就知道函数的作用,从而提高可读性.
很多时候,一个良好的子程序命名就是最好的注释,中国汉字看字形就可以知道其大概意思,这里看子程序的命名就可以知道这段要干些啥,从而实现了自我注释.
4.把简单的操作写成函数,方便修改
高质量子程序的具体指导意见:
1.功能上高内聚性:首先,子程序的命名要和其完成的功能相匹配,名字能够完全描述其功能,其次,一个子程序只做一项操作,可以极大地减少错误
程序如其名,叫什么就干什么,要保证语义上的一致.
2.关于命名:命名的要则是名字要可以反映程序的所有功能.子程序的名字可以跟随在对象后面,例如 对对象Car操作的子程序 CarStart(),对于过程性的子程序,用动词加名词的形式命名子程序比较好.而且要有对称的子程序,例如有create就有delete,有open就有close
常见的对称操作还有:
start/stop,
create/destroy
read/write
push/pop
等等
3.关于子程序的程度:最好限制在200行以内(不包括注释)
其实通常来说,一个子程序的长度最好不要超过一个屏幕大小可以容纳的代码行数,最好可以在一个屏幕的范围内把整个函数的所有代码显示出来,这样子不论是编写程序还是修改代码都比较方便.
PS.插个题外话,人类的大脑其实也有类似内存的概念,但是大脑的内存很小,大脑内同时记住的东西并不多,可以直接被人同一时间记住的东西通常只有5-7个,所以本着程序的根本目的是为了管理复杂性,在子程序内的变量个数最好不要超过8个.
4.关于子程序的参数设置:第一部分的参数用于输入数据,第二部分用于修改的数据,第三部分用于输出的数据,状态以及指示出错状态的变量也放在参数表最后,因为这些变量只是子程序的附属品,并且,不要把参数当做工作变量.参数的个数限制在7个以内
在实际中如果工程已经定义好了代码风格,就按已有的约定来设置子程序的参数
5.如果一个子函数的用途是用于返回由其名字所指明的返回值,那么就应该使用函数,否则应该使用过程
子函数和过程的区别在于:
子函数是为了干一件事然后返回一个结果,而过程不需要返回一个结果,过程只是仅仅把一件事给弄好而已,所以过程通常是以void func(…)这种形式出现的
6.在子函数中,检查所有可能的返回路径并且不要返回指向局部数据的引用或者指针
在C语言中宏和内联(inline)函数用的较多,代码大全里面也有提及到这些比较独特的地方
关于宏子程序和内联子程序
1. 要把宏表达式整个包含在括号内,如: #define f(x) x*x*x 则会出现问题,若x=a+1 则不会按照设计时的预想执行,要改为 #define f(x) ((x)*(x)*(x))
当语句超过一条的时候,可以采用大括号将宏语句全部括起来.
不过,如果可以,尽可能少用宏来代替函数
在不少面试题里面这一点都会被提及到,但是因为宏在C语言里面是在太有用了,君不见有些著名的开源库其实也用了宏来编写他们的库,例如libevent等,还有errno等也是必须用宏访问的
2. 由于inline子程序会破坏代码的封装原则,故少用inline子程序
因为编译器会将inline子程序全部把调用到inline函数的地方替换为inline函数的代码段,但是inline函数在优化函数调用这方面有不错的效果,给出一个例子:
int g_count = 0;
int foo(){
return g_count++;
}
int func1(){
return foo()+foo()+foo();
}
这个foo()函数会修改全局程序状态的一部分(它修改了全局变量g_count),所以编译器在编译的时候只能依次地执行3次foo(),得出结果为3
但是一旦将foo()用inline函数进行重写后:
inline int foo(){
return g_count++;
}
编译器就可以对程序进行优化了,一个可能的优化版本是:
int g_count = 0;
int func1(){
int temp= g_count++;
temp += g_count++;
temp += g_count++;
return temp;
}
甚至有可能编译器会统一对全局变量g_count进行更新,得出下面这个优化版本:
int func1(){
int temp = 3 * g_count + 3;
g_count += 3;
return temp;
}