相信读者已经大致了解具有良好可读性的代码应该是什么样子,或者可以大致体会出不同的编码风格对代码可读性带来的影响。
良好的编码风格(coding style)是极其重要的,Linux内核就是一个很好的例子。由于Linux内核的代码需要被成千上万的程序员阅读,因此为了保证代码的良好可读性,Linux内核在编码风格上的要求可以用严苛来形容。当我们向Linux内核提交一个拉取请求(pull request,我们可以将拉取请求理解为对代码进行修改之后的合并请求)时,如果编码风格不符合规定,则不管修改的内容如何,都会被拒绝。因此,我们在编写代码时一定要保证代码具有良好的编码风格。
那么,为了养成良好的编码风格,我们需要注意哪些方面呢?首先,代码应该满足良好的排版规则,包括缩进、空格、换行以及大括号的正确位置等。本章后面将以Linux内核的编码风格为例,详细介绍这方面的规则。
其次,代码应该遵循统一的命名规则。常见的命名规则有K&R命名法和驼峰命名法两种。K&R命名法是Kernighan和Ritchie在《C编程语言》一书中所采用的命名规则。这种规则用小写形式的单词和下画线组合对变量和函数进行命名,如
this_is_an_integer
。驼峰命名法源自匈牙利籍程序员Charles Simonyi在微软任职期间提出的匈牙利命名法,因微软在Win32 API及其示例代码中广泛采用这一命名规则而广为人知。匈牙利命名法约定了变量的命名由其类型缩写和首字母大写的单词组成,如
iThisIsAnInterger
。随后这一命名法被广泛用于其他编程语言,如Java、JavaScript等。在使用匈牙利命名法时,由于大写字母通常会凸出来,因此这种命名法在用于其他编程语言时,常被称为驼峰命名法。
Win32是Windows操作系统发展到3.0版本,开始支持32位的80386处理器时,微软为Windows操作系统设计的底层基础API所取的名字,意指针对32位平台的Windows API。
K&R命名法和驼峰命名法都是常用的命名规则,至于哪个好哪个坏,实在是“萝卜白菜各有所爱”的事情。但是在同一项目中,应该采用统一的规则,一般不建议混用。通常,K&R命名法在C代码中更为常见;而驼峰命名法除在Win32代码中之外,在面向对象编程语言(如C++、Java、Python)中更为常见,但有细微差别,比如在面向对象编程语言中,对属性或方法则经常使用
setLocale
这样的命名习惯(即首字母缩写)。
除了变量名和函数名,宏名、枚举常量名、全局变量名也应该遵循统一的命名规则。例如,宏名一般采用全大写形式,为避免混淆,通常会添加一些前缀,并使用下画线分隔不同的单词。枚举常量名则以字母K开头,表示常量(常量对应的constant一词的发音以k打头)。程序内部使用的全局变量,其名称前还会添加一两个下画线作为前缀,这样做是为了防止命名污染,避免一些潜在的问题(如符号冲突、链接错误等)。
通俗来讲,命名污染(naming pollution)就是指“重名”。重复的函数名、变量名、宏名会造成很多问题。简单的如编译警告或错误,或者链接生成可执行文件时报“重复的符号”错误,复杂的如重复的全局变量名和局部变量名导致程序执行异常,这些问题均可能需要程序员浪费大量时间来排查。由于设计上的问题,C语言一直未引入类似C++或其他编程语言对命名空间(naming space)的支持,这容易导致命名污染的产生。
编码风格的其他规则还包括自定义类型的使用、条件编译的写法、注释的写法,以及一些常见的约定写法。约定的写法通常来自经验的总结,比如如何处理系统可移植性、如何处理处理器架构的可移植性、如何实现国际化和本地化等。
众所周知,Linux内核的发展已逾30年。作为目前全球使用最为广泛的开源操作系统,一方面,Linux已经成为使用C语言编写大型基础软件工程项目的全球典范;另一方面,Linux也是目前最为成功的全球性开源协作项目。可以说,Linux内核是每一位C程序员取之不尽、用之不竭的宝库。作为一名C程序员,学习Linux内核的编码风格,并有意向其靠近,是绝对正确的选择。
这里以缩进和注释为例介绍Linux内核的编码风格。完整的Linux内核编码风格文档,可通过在任意搜索引擎中搜索
Linux kernel coding style
阅读。
Linux内核的编码风格规定,代码的缩进应该采用制表符(Tab),宽度为8个空格(和Windows操作系统中大多数编辑器或字处理软件常使用4个空格不同)。例如:
int system_is_up(void) { return system_state == SYSTEM_RUNNING; }
这个规定有什么好处呢?Linux内核的编码风格还规定每一行代码的长度不得超过80个字符。按照“Linux之父”Linus的说法,如果代码的缩进或嵌套超过3个层次,就要考虑对代码进行重构。因此 8 个空格的缩进可以有效地限制缩进或嵌套的层次,迫使程序员在编写代码时不要采用过多的嵌套或缩进。
当然,8个空格的缩进要求确实有些严苛,读者也可以酌情在自己的代码中采用4个空格的缩进,但4个空格应该是底线,再少就不合适了。
Linux内核的编码风格还禁止采用C编程语言的“//”注释方式(这和Linus不喜欢C++编程语言有一些关系)。Linux内核采用的单行注释形式如下:
/* This is a single-line comment. */
Linux内核采用多行注释形式如下:
/* * This is the preferred style for multi-line * comments in the Linux kernel source code. * Please use it consistently. * * Description: A column of asterisks on the left side, * with beginning and ending almost-blank lines. */
除此之外,在Linux内核的编码风格中,还特别说明不要针对结构体使用
typedef
定义新的数据类型,而应该始终使用
struct foo
这样的形式。
作为一个重要的经验性约定,Linux内核的编码风格要求所有的函数在末尾集中返回。也就是说,应避免在一个函数的头部或中部使用
return
语句。因此,我们可以在Linux内核的源代码中看到被传统编程教材强烈否定的
goto
语句的频繁使用。本章后面将解释这一做法的好处。这类规定属于经验性约定,在某种程度上可以规范某些编码行为,从而提高代码的一致性和质量,属于可维护性的范畴。
除了Linux内核的编码风格,流传较为广泛的还有GNU的C语言编码风格(后文简称GNU编码风格),以及Win32编码风格等。
GNU是GNU’s Not UNIX的递归缩写,它是由自由软件基金会(Free Software Foundation,FSF)于20世纪80年代发起的一个重要的自由软件项目。GNU项目的目标是开发一个自由的UNIX变种HURD。尽管HURD的开发处在停滞状态,但GNU项目开发和维护着许多高质量的基础软件和工具,其中包括基础库(Glibc)、编译器(GCC)、编辑器(Emacs)以及各类命令行工具。这些软件至今仍然在我们的软件世界中扮演着非常重要的角色。GNU项目的大部分软件是使用C语言开发的。
GNU编码风格同样要求每行代码的字符不能超过80个,但在排版上和Linux内核的编码风格有较大的区别,尤其在缩进、空格和大括号的位置方面区别较大。比如下面这段使用GNU编码风格的代码:
int lots_of_args (int x, long y, short z, double a_double, float a_float) { int haha = 0; if (x < foo (y, z)) haha = bar[4] + 5; else { while (z) { haha += foo (z, z); z--; } return ++x + bar (); } return haha; }
如果按照Linux内核的编码风格,则应修改为如下形式:
int lots_of_args(int x, long y, short z, double a_double, float a_float) { int haha = 0; if (x < foo(y, z)) { haha = bar[4] + 5; } else { while (z) { haha += foo(z, z); z--; } haha = ++x + bar(); } return haha; }
除了排版、注释、语法约定、命名等属于代码可读性范畴的内容,GNU的编码风格还包括如下方面的一些规定。
(1)系统可移植性:规定了如何处理跨操作系统的可移植性。
(2)处理器可移植性:规定了如何应对不同种类的处理器或处理器架构。
(3)系统函数:规定了如何应对不同平台在标准函数库上的差异。
(4)规定了国际化、字符集、引号的使用以及
mmap
函数的使用等。
这些规定是经验性约定,属于代码可维护性的范畴。
Win32编码风格并不像GNU编码风格或者Linux内核的编码风格那样存在一个在线可查阅的版本,而是散见于Win32头文件以及各种示例程序中。我们可以将Win32编码风格理解成匈牙利命名法的集大成者。
在开源项目MiniGUI中,由于其API模仿Win32而来,因此其编码风格极具匈牙利命名法风格。下面是MiniGUI中
ShowWindow()
函数的实现代码。
/* ** This function shows window in behavious by specified iCmdShow. */ BOOL GUIAPI ShowWindow (HWND hWnd, int iCmdShow) { MG_CHECK_RET (MG_IS_NORMAL_WINDOW(hWnd), FALSE); if (IsMainWindow (hWnd)) { ... } else { PCONTROL pControl; pControl = (PCONTROL)hWnd; if (pControl->dwExStyle & WS_EX_CTRLASMAINWIN) { ... } else { switch (iCmdShow) { case SW_HIDE: if (pControl->dwStyle & WS_VISIBLE) { pControl->dwStyle &= ~WS_VISIBLE; InvalidateRect ((HWND)(pControl->pParent), (RECT*)(&pControl->left), TRUE); } break; ... } } if (iCmdShow == SW_HIDE && pControl->pParent->active == pControl) { SendNotifyMessage (hWnd, MSG_KILLFOCUS, 0, 0); pControl->pParent->active = NULL; } } SendNotifyMessage (hWnd, MSG_SHOWWINDOW, (WPARAM)iCmdShow, 0); return TRUE; }
从上述代码中可以看出,Win32编码风格的典型特征如下。
● 使用匈牙利命名法命名变量,如上述代码中的
iCmdShow
和
pControl
,前者表示整型变量,后者表示指针。
● 使用匈牙利命名法命名函数,如上述代码中的
SendNotifyMessage()
和
InvalidateRect()
。
● 较多地使用了类型定义,如上述代码中的
HWND
、
BOOL
、
WPARAM
、
RECT
等;其中的
RECT
是一个结构体,它定义了一个矩形的左上角和右下角两个顶点的坐标。
值得一提的是,上面的代码对字符数超过80的行做了绕行处理,但Win32编码风格对这一点并不作硬性限制。毕竟Windows平台为开发者提供了图形化的集成开发环境,每行字符超过80个并不会给程序员带来很大的困扰。另外,由于Win32 API和变量名通常较长,因此一行代码很容易超过80个字符。
随着Windows平台上的主流编程语言从C转为C++,而后又转为C#,现在使用Win32编码风格的C代码已经相对少见了。